agora inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v16 2/2] handle relation statistics correctly during rewrites
144+ messages / 2 participants
[nested] [flat]

* [PATCH v16 2/2] handle relation statistics correctly during rewrites
@ 2026-05-18 15:20  Bertrand Drouvot <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Bertrand Drouvot @ 2026-05-18 15:20 UTC (permalink / raw)

This patch ensures that relation statistics are preserved correctly when a
relation is rewritten (e.g., during CLUSTER, VACUUM FULL, ALTER TABLE
operations that require a table rewrite, or TRUNCATE).

Since patch 0001 keys PGSTAT_KIND_RELATION by the physical RelFileLocator
rather than the relation OID, a rewrite (which assigns a new relfilenode)
would lose the accumulated statistics if not handled specially.

The approach is:

1. When a rewrite occurs (RelationSetNewRelfilenumber or swap_relation_files),
   record the old and new locators via pgstat_mark_rewrite().

2. During pgstat_assoc_relation(), if the relation's new locator matches a
   pending rewrite, continue using the stats entry associated with the old
   locator. This ensures stats accumulated after the rewrite are still
   tracked in the original entry.

3. At transaction commit (AtEOXact_PgStat_Relations), process pending
   rewrites: flush any pending stats for the old locator, copy them to the
   new locator's stats entry, and drop the old entry.

4. At subtransaction abort, remove rewrites from the aborted nesting level.

5. For two-phase transactions, the rewrite information is recorded in the
   TwoPhasePgStatRecord and replayed at commit/abort time.

This also handles chained rewrites (multiple rewrites of the same relation
in a single transaction) by tracking the original locator through the chain.
---
 src/backend/catalog/index.c                  |   2 +-
 src/backend/commands/repack.c                |   5 +
 src/backend/commands/tablecmds.c             |   6 +
 src/backend/utils/activity/pgstat_relation.c | 392 ++++++++++++++++++-
 src/backend/utils/activity/pgstat_xact.c     |  25 +-
 src/backend/utils/cache/relcache.c           |   6 +
 src/include/pgstat.h                         |   5 +-
 src/tools/pgindent/typedefs.list             |   1 +
 8 files changed, 425 insertions(+), 17 deletions(-)
  92.8% src/backend/utils/activity/
   4.8% src/backend/

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 9407c357f27..335ff1e50f6 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1813,7 +1813,7 @@ index_concurrently_swap(Oid newIndexId, Oid oldIndexId, const char *oldName)
 	changeDependenciesOn(RelationRelationId, oldIndexId, newIndexId);
 
 	/* copy over statistics from old to new index */
-	pgstat_copy_relation_stats(newClassRel, oldClassRel);
+	pgstat_copy_relation_stats(newClassRel->rd_locator, oldClassRel->rd_locator, false);
 
 	/* Copy data of pg_statistic from the old index to the new one */
 	CopyStatistics(oldIndexId, newIndexId);
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 351a3cc32e8..eda4135bcd1 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -1633,6 +1633,11 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 
 		rel1 = relation_open(r1, NoLock);
 		rel2 = relation_open(r2, NoLock);
+
+		/* Mark that a rewrite happened */
+		if (RELKIND_HAS_STORAGE(rel1->rd_rel->relkind))
+			pgstat_mark_rewrite(rel1->rd_locator, rel2->rd_locator);
+
 		rel2->rd_createSubid = rel1->rd_createSubid;
 		rel2->rd_newRelfilelocatorSubid = rel1->rd_newRelfilelocatorSubid;
 		rel2->rd_firstRelfilelocatorSubid = rel1->rd_firstRelfilelocatorSubid;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 92b0f38c353..b553b9a4970 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -17129,6 +17129,7 @@ ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode)
 	Oid			reltoastrelid;
 	RelFileNumber newrelfilenumber;
 	RelFileLocator newrlocator;
+	RelFileLocator oldrlocator;
 	List	   *reltoastidxids = NIL;
 	ListCell   *lc;
 
@@ -17167,6 +17168,7 @@ ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode)
 	newrlocator = rel->rd_locator;
 	newrlocator.relNumber = newrelfilenumber;
 	newrlocator.spcOid = newTableSpace;
+	oldrlocator = rel->rd_locator;
 
 	/* hand off to AM to actually create new rel storage and copy the data */
 	if (rel->rd_rel->relkind == RELKIND_INDEX)
@@ -17179,6 +17181,10 @@ ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode)
 		table_relation_copy_data(rel, &newrlocator);
 	}
 
+	/* mark that a rewrite happened */
+	if (RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
+		pgstat_mark_rewrite(oldrlocator, newrlocator);
+
 	/*
 	 * Update the pg_class row.
 	 *
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 7fcda7f7518..18d1b9fc59d 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -30,6 +30,19 @@
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
 
+/* Pending rewrite operations for stats copying */
+typedef struct PgStat_PendingRewrite
+{
+	RelFileLocator old_locator;
+	RelFileLocator new_locator;
+	RelFileLocator original_locator;
+	int			nest_level;		/* Transaction nesting level where rewrite
+								 * occurred */
+	struct PgStat_PendingRewrite *next;
+} PgStat_PendingRewrite;
+
+/* The pending rewrites list for current transaction */
+static PgStat_PendingRewrite *pending_rewrites = NULL;
 
 /* Record that's written to 2PC state file when pgstat state is persisted */
 typedef struct TwoPhasePgStatRecord
@@ -43,6 +56,8 @@ typedef struct TwoPhasePgStatRecord
 	PgStat_Counter deleted_pre_truncdrop;
 	RelFileLocator locator;		/* table's rd_locator */
 	bool		truncdropped;	/* was the relation truncated/dropped? */
+	RelFileLocator rewrite_old_locator;
+	int			rewrite_nest_level;
 } TwoPhasePgStatRecord;
 
 
@@ -54,27 +69,71 @@ static void restore_truncdrop_counters(PgStat_TableXactStatus *trans);
 
 
 /*
- * Copy stats between relations. This is used for things like REINDEX
+ * Copy stats between RelFileLocator. This is used for things like REINDEX
  * CONCURRENTLY.
  */
 void
-pgstat_copy_relation_stats(Relation dst, Relation src)
+pgstat_copy_relation_stats(RelFileLocator dst, RelFileLocator src, bool increment)
 {
 	PgStat_StatTabEntry *srcstats;
 	PgStatShared_Relation *dstshstats;
 	PgStat_EntryRef *dst_ref;
 
-	srcstats = pgstat_fetch_stat_tabentry_ext(RelationGetRelid(src), NULL);
+	srcstats = (PgStat_StatTabEntry *) pgstat_fetch_entry(PGSTAT_KIND_RELATION,
+														  src.dbOid,
+														  RelFileLocatorToPgStatObjid(src),
+														  NULL);
 	if (!srcstats)
 		return;
 
 	dst_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
-										  dst->rd_rel->relisshared ? InvalidOid : MyDatabaseId,
-										  RelationGetRelid(dst),
+										  dst.dbOid,
+										  RelFileLocatorToPgStatObjid(dst),
 										  false);
 
 	dstshstats = (PgStatShared_Relation *) dst_ref->shared_stats;
-	dstshstats->stats = *srcstats;
+
+	if (!increment)
+		dstshstats->stats = *srcstats;
+	else
+	{
+		/* Increment those statistics */
+#define RELFSTAT_ACC(fld, stats_to_add) \
+		(dstshstats->stats.fld += stats_to_add->fld)
+		RELFSTAT_ACC(numscans, srcstats);
+		RELFSTAT_ACC(tuples_returned, srcstats);
+		RELFSTAT_ACC(tuples_fetched, srcstats);
+		RELFSTAT_ACC(tuples_inserted, srcstats);
+		RELFSTAT_ACC(tuples_updated, srcstats);
+		RELFSTAT_ACC(tuples_deleted, srcstats);
+		RELFSTAT_ACC(tuples_hot_updated, srcstats);
+		RELFSTAT_ACC(tuples_newpage_updated, srcstats);
+		RELFSTAT_ACC(live_tuples, srcstats);
+		RELFSTAT_ACC(dead_tuples, srcstats);
+		RELFSTAT_ACC(mod_since_analyze, srcstats);
+		RELFSTAT_ACC(ins_since_vacuum, srcstats);
+		RELFSTAT_ACC(blocks_fetched, srcstats);
+		RELFSTAT_ACC(blocks_hit, srcstats);
+		RELFSTAT_ACC(vacuum_count, srcstats);
+		RELFSTAT_ACC(autovacuum_count, srcstats);
+		RELFSTAT_ACC(analyze_count, srcstats);
+		RELFSTAT_ACC(autoanalyze_count, srcstats);
+		RELFSTAT_ACC(total_vacuum_time, srcstats);
+		RELFSTAT_ACC(total_autovacuum_time, srcstats);
+		RELFSTAT_ACC(total_analyze_time, srcstats);
+		RELFSTAT_ACC(total_autoanalyze_time, srcstats);
+#undef RELFSTAT_ACC
+
+		/* Replace those statistics */
+#define RELFSTAT_REP(fld, stats_to_rep) \
+		(dstshstats->stats.fld = stats_to_rep->fld)
+		RELFSTAT_REP(lastscan, srcstats);
+		RELFSTAT_REP(last_vacuum_time, srcstats);
+		RELFSTAT_REP(last_autovacuum_time, srcstats);
+		RELFSTAT_REP(last_analyze_time, srcstats);
+		RELFSTAT_REP(last_autoanalyze_time, srcstats);
+#undef RELFSTAT_REP
+	}
 
 	pgstat_unlock_entry(dst_ref);
 }
@@ -136,6 +195,7 @@ void
 pgstat_assoc_relation(Relation rel)
 {
 	RelFileLocator locator;
+	PgStat_TableStatus *pgstat_info;
 
 	Assert(rel->pgstat_enabled);
 	Assert(rel->pgstat_info == NULL);
@@ -164,14 +224,54 @@ pgstat_assoc_relation(Relation rel)
 		locator.relNumber = rel->rd_id;
 	}
 
+	/*
+	 * If this relation was rewritten during the current transaction we may be
+	 * reopening it with its new RelFileLocator. In that case, continue using
+	 * the stats entry associated with the old locator rather than creating a
+	 * new one. This ensures all stats from before and after the rewrite are
+	 * tracked in a single entry which will be properly copied to the new
+	 * locator at transaction commit.
+	 */
+	if (pending_rewrites != NULL)
+	{
+		PgStat_PendingRewrite *rewrite;
+
+		for (rewrite = pending_rewrites; rewrite != NULL; rewrite = rewrite->next)
+		{
+			if (locator.dbOid == rewrite->new_locator.dbOid &&
+				locator.spcOid == rewrite->new_locator.spcOid &&
+				locator.relNumber == rewrite->new_locator.relNumber)
+			{
+				pgstat_info = pgstat_prep_relation_pending(rewrite->old_locator);
+				goto found_entry;
+			}
+		}
+	}
+
 	/* Else find or make the PgStat_TableStatus entry, and update link */
-	rel->pgstat_info = pgstat_prep_relation_pending(locator);
+	pgstat_info = pgstat_prep_relation_pending(locator);
+
+found_entry:
+	rel->pgstat_info = pgstat_info;
+
+	/*
+	 * For relations stats, we key by physical file location, not by relation
+	 * OID. This means during operations like ALTER TYPE it's possible that
+	 * the relation OID changes but the relfilenode stays the same (no actual
+	 * rewrite needed). Unlink the old relation first.
+	 */
+	if (pgstat_info->relation != NULL &&
+		pgstat_info->relation != rel)
+	{
+		pgstat_info->relation->pgstat_info = NULL;
+		pgstat_info->relation = NULL;
+	}
 
 	/* don't allow link a stats to multiple relcache entries */
-	Assert(rel->pgstat_info->relation == NULL);
+	Assert(pgstat_info->relation == NULL);
 
 	/* mark this relation as the owner */
-	rel->pgstat_info->relation = rel;
+	pgstat_info->relation = rel;
 }
 
 /*
@@ -214,14 +314,37 @@ pgstat_drop_relation(Relation rel)
 {
 	int			nest_level = GetCurrentTransactionNestLevel();
 	PgStat_TableStatus *pgstat_info;
+	bool		skip_transactional_drop = false;
 
 	/* don't track stats for relations without storage */
 	if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
 		return;
 
-	pgstat_drop_transactional(PGSTAT_KIND_RELATION,
-							  rel->rd_locator.dbOid,
-							  RelFileLocatorToPgStatObjid(rel->rd_locator));
+	/* Check if this drop is part of a pending rewrite */
+	if (pending_rewrites != NULL)
+	{
+		PgStat_PendingRewrite *rewrite;
+
+		for (rewrite = pending_rewrites; rewrite != NULL; rewrite = rewrite->next)
+		{
+			if (rel->rd_locator.dbOid == rewrite->old_locator.dbOid &&
+				rel->rd_locator.spcOid == rewrite->old_locator.spcOid &&
+				rel->rd_locator.relNumber == rewrite->old_locator.relNumber)
+			{
+				skip_transactional_drop = true;
+				break;
+			}
+		}
+	}
+
+	/*
+	 * If it is part of a rewrite, drop its stats later, for example in
+	 * AtEOXact_PgStat_Relations(), so skip it here.
+	 */
+	if (!skip_transactional_drop)
+		pgstat_drop_transactional(PGSTAT_KIND_RELATION,
+								  rel->rd_locator.dbOid,
+								  RelFileLocatorToPgStatObjid(rel->rd_locator));
 
 	if (!pgstat_should_count_relation(rel))
 		return;
@@ -668,6 +791,48 @@ AtEOXact_PgStat_Relations(PgStat_SubXactStatus *xact_state, bool isCommit)
 		}
 		tabstat->trans = NULL;
 	}
+
+	/* preserve the stats in case of rewrite */
+	if (isCommit && pending_rewrites != NULL)
+	{
+		PgStat_PendingRewrite *rewrite;
+		PgStat_PendingRewrite *prev = NULL;
+		PgStat_PendingRewrite *current = pending_rewrites;
+		PgStat_PendingRewrite *next;
+
+		/* reverse the rewrites list to process in chronological order */
+		while (current != NULL)
+		{
+			next = current->next;
+			current->next = prev;
+			prev = current;
+			current = next;
+		}
+
+		/* now process rewrites in chronological order */
+		for (rewrite = prev; rewrite != NULL; rewrite = rewrite->next)
+		{
+			PgStat_EntryRef *old_entry_ref;
+
+			old_entry_ref = pgstat_fetch_pending_entry(PGSTAT_KIND_RELATION,
+													   rewrite->old_locator.dbOid,
+													   RelFileLocatorToPgStatObjid(rewrite->old_locator));
+
+			if (old_entry_ref && old_entry_ref->pending)
+				pgstat_relation_flush_cb(old_entry_ref, false);
+
+			pgstat_copy_relation_stats(rewrite->new_locator,
+									   rewrite->old_locator, true);
+
+			/* drop old locator's stats */
+			if (!pgstat_drop_entry(PGSTAT_KIND_RELATION,
+								   rewrite->old_locator.dbOid,
+								   RelFileLocatorToPgStatObjid(rewrite->old_locator)))
+				pgstat_request_entry_refs_gc();
+		}
+	}
+
+	pending_rewrites = NULL;
 }
 
 /*
@@ -683,6 +848,30 @@ AtEOSubXact_PgStat_Relations(PgStat_SubXactStatus *xact_state, bool isCommit, in
 	PgStat_TableXactStatus *trans;
 	PgStat_TableXactStatus *next_trans;
 
+	/*
+	 * If we don't commit then remove the associated rewrites if any, to keep
+	 * the rewrite chain in sync with what will be eventually committed.
+	 */
+	if (!isCommit)
+	{
+		PgStat_PendingRewrite **rewrite_ptr = &pending_rewrites;
+
+		while (*rewrite_ptr != NULL)
+		{
+			if ((*rewrite_ptr)->nest_level >= nestDepth)
+			{
+				PgStat_PendingRewrite *to_remove = *rewrite_ptr;
+
+				*rewrite_ptr = (*rewrite_ptr)->next;
+				pfree(to_remove);
+			}
+			else
+			{
+				rewrite_ptr = &((*rewrite_ptr)->next);
+			}
+		}
+	}
+
 	for (trans = xact_state->first; trans != NULL; trans = next_trans)
 	{
 		PgStat_TableStatus *tabstat;
@@ -762,11 +951,19 @@ void
 AtPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state)
 {
 	PgStat_TableXactStatus *trans;
+	PgStat_PendingRewrite *rewrite;
 
+	/*
+	 * For each tabstat, find its matching rewrite and remove it from the
+	 * pending rewrites list. This way, after processing all tabstats, pending
+	 * rewrites will only contain rewrite only transactions.
+	 */
 	for (trans = xact_state->first; trans != NULL; trans = trans->next)
 	{
 		PgStat_TableStatus *tabstat PG_USED_FOR_ASSERTS_ONLY;
 		TwoPhasePgStatRecord record;
+		PgStat_PendingRewrite **rewrite_ptr;
+		bool		found_rewrite = false;
 
 		Assert(trans->nest_level == 1);
 		Assert(trans->upper == NULL);
@@ -786,10 +983,83 @@ AtPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state)
 			record.locator = tabstat->locator;
 
 		record.truncdropped = trans->truncdropped;
+		record.rewrite_nest_level = 0;
+
+		/*
+		 * Look for a matching rewrite and remove it from pending rewrites. We
+		 * check three possible matches:
+		 *
+		 * The new_locator when stats have been added after the rewrite. The
+		 * old_locator when stats have been added before the rewrite but not
+		 * after. The original_locator when this tabstat is part of a rewrite
+		 * chain.
+		 */
+		rewrite_ptr = &pending_rewrites;
+		while (*rewrite_ptr != NULL)
+		{
+			rewrite = *rewrite_ptr;
+
+			if ((record.locator.dbOid == rewrite->new_locator.dbOid &&
+				 record.locator.spcOid == rewrite->new_locator.spcOid &&
+				 record.locator.relNumber == rewrite->new_locator.relNumber) ||
+				(tabstat->locator.dbOid == rewrite->old_locator.dbOid &&
+				 tabstat->locator.spcOid == rewrite->old_locator.spcOid &&
+				 tabstat->locator.relNumber == rewrite->old_locator.relNumber) ||
+				(tabstat->locator.dbOid == rewrite->original_locator.dbOid &&
+				 tabstat->locator.spcOid == rewrite->original_locator.spcOid &&
+				 tabstat->locator.relNumber == rewrite->original_locator.relNumber))
+			{
+				/*
+				 * Found matching rewrite. Record the rewrite information and
+				 * remove this rewrite from the list since it's now handled.
+				 */
+				record.rewrite_old_locator = rewrite->original_locator;
+				record.rewrite_nest_level = rewrite->nest_level;
+				record.locator = rewrite->new_locator;
+				found_rewrite = true;
+
+				/* Remove from pending_rewrites list */
+				*rewrite_ptr = rewrite->next;
+				pfree(rewrite);
+				break;
+			}
+			else
+			{
+				/* Move to next rewrite in the list */
+				rewrite_ptr = &(rewrite->next);
+			}
+		}
+
+		/* If no rewrite found, clear the rewrite fields */
+		if (!found_rewrite)
+		{
+			memset(&record.rewrite_old_locator, 0, sizeof(RelFileLocator));
+		}
+
+		RegisterTwoPhaseRecord(TWOPHASE_RM_PGSTAT_ID, 0,
+							   &record, sizeof(TwoPhasePgStatRecord));
+	}
+
+	/*
+	 * Now process any rewrites still pending. These are rewrite only
+	 * transactions. We need to preserve their stats even though there's no
+	 * tabstat entry for them.
+	 */
+	for (rewrite = pending_rewrites; rewrite != NULL; rewrite = rewrite->next)
+	{
+		TwoPhasePgStatRecord record;
+
+		memset(&record, 0, sizeof(TwoPhasePgStatRecord));
+		record.locator = rewrite->new_locator;
+		record.rewrite_old_locator = rewrite->original_locator;
+		record.rewrite_nest_level = rewrite->nest_level;
+		record.truncdropped = false;
 
 		RegisterTwoPhaseRecord(TWOPHASE_RM_PGSTAT_ID, 0,
 							   &record, sizeof(TwoPhasePgStatRecord));
 	}
+
+	pending_rewrites = NULL;
 }
 
 /*
@@ -812,6 +1082,8 @@ PostPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state)
 		tabstat = trans->parent;
 		tabstat->trans = NULL;
 	}
+
+	pending_rewrites = NULL;
 }
 
 /*
@@ -847,6 +1119,29 @@ pgstat_twophase_postcommit(FullTransactionId fxid, uint16 info,
 	pgstat_info->counts.changed_tuples +=
 		rec->tuples_inserted + rec->tuples_updated +
 		rec->tuples_deleted;
+
+	if (rec->rewrite_nest_level > 0)
+	{
+		PgStat_EntryRef *old_entry_ref;
+
+		/* Flush any pending stats for old locator first */
+		old_entry_ref = pgstat_fetch_pending_entry(PGSTAT_KIND_RELATION,
+												   rec->rewrite_old_locator.dbOid,
+												   RelFileLocatorToPgStatObjid(rec->rewrite_old_locator));
+
+		if (old_entry_ref && old_entry_ref->pending)
+			pgstat_relation_flush_cb(old_entry_ref, false);
+
+		/* Copy stats from old to new locator */
+		pgstat_copy_relation_stats(rec->locator, rec->rewrite_old_locator,
+								   true);
+
+		/* Drop old locator's stats */
+		if (!pgstat_drop_entry(PGSTAT_KIND_RELATION,
+							   rec->rewrite_old_locator.dbOid,
+							   RelFileLocatorToPgStatObjid(rec->rewrite_old_locator)))
+			pgstat_request_entry_refs_gc();
+	}
 }
 
 /*
@@ -861,9 +1156,26 @@ pgstat_twophase_postabort(FullTransactionId fxid, uint16 info,
 {
 	TwoPhasePgStatRecord *rec = (TwoPhasePgStatRecord *) recdata;
 	PgStat_TableStatus *pgstat_info;
+	RelFileLocator target_locator;
+
+	/*
+	 * For aborted transactions with rewrites (like TRUNCATE), we need to
+	 * restore stats to the old locator, not the new one. The new locator
+	 * should be dropped since the rewrite is being rolled back.
+	 */
+	if (rec->rewrite_nest_level > 0)
+	{
+		/* Use the old locator */
+		target_locator = rec->rewrite_old_locator;
+	}
+	else
+	{
+		/* No rewrite, use the original locator */
+		target_locator = rec->locator;
+	}
 
 	/* Find or create a tabstat entry for the target locator */
-	pgstat_info = pgstat_prep_relation_pending(rec->locator);
+	pgstat_info = pgstat_prep_relation_pending(target_locator);
 
 	/* Same math as in AtEOXact_PgStat, abort case */
 	if (rec->truncdropped)
@@ -918,7 +1230,17 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 	tabentry->numscans += lstats->counts.numscans;
 	if (lstats->counts.numscans)
 	{
-		TimestampTz t = GetCurrentTransactionStopTimestamp();
+		TimestampTz t;
+
+		/*
+		 * Checking the transaction state due to the flush call in
+		 * pgstat_twophase_postcommit() that would break the assertion on the
+		 * state in GetCurrentTransactionStopTimestamp().
+		 */
+		if (!IsTransactionState())
+			t = GetCurrentTransactionStopTimestamp();
+		else
+			t = GetCurrentTimestamp();
 
 		if (t > tabentry->lastscan)
 			tabentry->lastscan = t;
@@ -1169,3 +1491,45 @@ pgstat_reloid_to_relfilelocator(Oid reloid, RelFileLocator *locator)
 	ReleaseSysCache(tuple);
 	return result;
 }
+
+/*
+ * Mark that a relation rewrite has occurred, preserving the original locator
+ * so stats can be copied at transaction commit.
+ */
+void
+pgstat_mark_rewrite(RelFileLocator old_locator, RelFileLocator new_locator)
+{
+	PgStat_PendingRewrite *rewrite;
+	PgStat_PendingRewrite *existing;
+	RelFileLocator original_locator = old_locator;
+
+	for (existing = pending_rewrites; existing != NULL; existing = existing->next)
+	{
+		if (old_locator.dbOid == existing->new_locator.dbOid &&
+			old_locator.spcOid == existing->new_locator.spcOid &&
+			old_locator.relNumber == existing->new_locator.relNumber)
+		{
+			original_locator = existing->original_locator;
+			break;
+		}
+	}
+
+	/* Allocate in TopTransactionContext memory context */
+	rewrite = MemoryContextAlloc(TopTransactionContext,
+								 sizeof(PgStat_PendingRewrite));
+
+	rewrite->old_locator = old_locator;
+	rewrite->new_locator = new_locator;
+	rewrite->original_locator = original_locator;
+	rewrite->nest_level = GetCurrentTransactionNestLevel();
+
+	/* Add to the list */
+	rewrite->next = pending_rewrites;
+	pending_rewrites = rewrite;
+}
+
+void
+pgstat_clear_rewrite(void)
+{
+	pending_rewrites = NULL;
+}
diff --git a/src/backend/utils/activity/pgstat_xact.c b/src/backend/utils/activity/pgstat_xact.c
index 5e2d69e6297..8ed8f5317f3 100644
--- a/src/backend/utils/activity/pgstat_xact.c
+++ b/src/backend/utils/activity/pgstat_xact.c
@@ -55,6 +55,8 @@ AtEOXact_PgStat(bool isCommit, bool parallel)
 	}
 	pgStatXactStack = NULL;
 
+	pgstat_clear_rewrite();
+
 	/* Make sure any stats snapshot is thrown away */
 	pgstat_clear_snapshot();
 }
@@ -360,8 +362,29 @@ create_drop_transactional_internal(PgStat_Kind kind, Oid dboid, uint64 objid, bo
 void
 pgstat_create_transactional(PgStat_Kind kind, Oid dboid, uint64 objid)
 {
-	if (pgstat_get_entry_ref(kind, dboid, objid, false, NULL))
+	PgStat_EntryRef *entry_ref;
+
+	entry_ref = pgstat_get_entry_ref(kind, dboid, objid, false, NULL);
+
+	if (entry_ref)
 	{
+		/*
+		 * For relations stats, we key by physical file location, not by
+		 * relation OID. This means during operations like ALTER TYPE where
+		 * the relation OID changes but the relfilenode stays the same (no
+		 * actual rewrite needed), we'll find an existing entry.
+		 *
+		 * This is expected behavior, we want to preserve stats across the
+		 * catalog change. Simply reset and recreate the entry for the new
+		 * relation OID without warning.
+		 */
+		if (kind == PGSTAT_KIND_RELATION)
+		{
+			pgstat_reset(kind, dboid, objid);
+			create_drop_transactional_internal(kind, dboid, objid, true);
+			return;
+		}
+
 		ereport(WARNING,
 				errmsg("resetting existing statistics for kind %s, db=%u, oid=%" PRIu64,
 					   (pgstat_get_kind_info(kind))->name, dboid,
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0572ab424e7..d84058db102 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -87,6 +87,7 @@
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/pgstat_internal.h"
 #include "utils/relmapper.h"
 #include "utils/resowner.h"
 #include "utils/snapmgr.h"
@@ -3782,6 +3783,7 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 	MultiXactId minmulti = InvalidMultiXactId;
 	TransactionId freezeXid = InvalidTransactionId;
 	RelFileLocator newrlocator;
+	RelFileLocator oldrlocator = relation->rd_locator;
 
 	if (!IsBinaryUpgrade)
 	{
@@ -3953,6 +3955,10 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 
 	table_close(pg_class, RowExclusiveLock);
 
+	/* Mark that a rewrite happened */
+	if (RELKIND_HAS_STORAGE(relation->rd_rel->relkind))
+		pgstat_mark_rewrite(oldrlocator, newrlocator);
+
 	/*
 	 * Make the pg_class row change or relation map change visible.  This will
 	 * cause the relcache entry to get updated, too.
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 941a39bd16e..5344ece9ecf 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -697,7 +697,7 @@ extern PgStat_FunctionCounts *find_funcstat_entry(Oid func_id);
 
 extern void pgstat_create_relation(Relation rel);
 extern void pgstat_drop_relation(Relation rel);
-extern void pgstat_copy_relation_stats(Relation dst, Relation src);
+extern void pgstat_copy_relation_stats(RelFileLocator dst, RelFileLocator src, bool increment);
 
 extern void pgstat_init_relation(Relation rel);
 extern void pgstat_assoc_relation(Relation rel);
@@ -709,6 +709,9 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_mark_rewrite(RelFileLocator old_locator,
+								RelFileLocator new_locator);
+extern void pgstat_clear_rewrite(void);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..1544f54e918 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2349,6 +2349,7 @@ PgStat_LockEntry
 PgStat_PendingDroppedStatsItem
 PgStat_PendingIO
 PgStat_PendingLock
+PgStat_PendingRewrite
 PgStat_SLRUStats
 PgStat_ShmemControl
 PgStat_Snapshot
-- 
2.34.1


--Uc+t0mng6H5VV2zk--






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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-06-16 11:54  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 858 ++++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 733 insertions(+), 141 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 28b34fd4387..6f3e4ecff46 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -50,6 +50,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -94,6 +95,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -118,6 +151,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -132,6 +172,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -173,15 +221,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static Relation process_single_relation(RepackStmt *stmt,
@@ -507,7 +570,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -525,11 +587,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -651,6 +708,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -662,9 +730,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -986,13 +1051,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1003,8 +1069,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1028,8 +1101,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1039,98 +1116,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		/*
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
+		 */
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1194,6 +1205,323 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1208,7 +1536,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1219,6 +1547,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1258,7 +1587,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2554,31 +2885,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3413,6 +3719,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3430,21 +3906,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3467,13 +3949,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3632,9 +4118,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3655,8 +4147,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3715,7 +4215,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3724,6 +4230,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3735,20 +4243,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		newindex = build_new_index(rel_new, rel_old, oldindex);
+
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3772,17 +4307,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -3876,6 +4456,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c0f6217caa6..ca60e4123b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6064,7 +6064,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 90234c3f736..c08f0524ea4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -430,6 +430,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2607,6 +2608,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v01-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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

* [PATCH 6/8] Use separate transactions for catalog changes.
@ 2026-07-15 08:37  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 144+ messages in thread

From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw)

This is another part of the effort to eliminate the impact of REPACK
(CONCURRENTLY) on VACUUM xmin horizon. While one of the previous patches
avoids using the same snapshot for long time, here we try to avoid using the
same XID throughout the REPACK execution. The idea is that we only get XID
assigned for a catalog change, and as soon as it's done, we start a new
transaction. We still need XID to load the data into the new table, but this
will be fixed by the next patch in the series.

A side effect of committing the catalog changes early is that the new table is
visible to other transactions while REPACK is still running. The other
transactions are not able to access it though since we keep it locked in
AccessExclusiveMode all the time. However, by committing the transaction that
created the table we'd lose the lock. Therefore we use a session lock until
the table gets locked in the following transaction.

Another problem is that, once the creation of the new table got committed, the
table will not be dropped automatically if REPACK ends up with ERROR (or
crash). We resolve this by creating a dependency in the system catalog. When
REPACK starts, it first uses the dependency information to find out if some
table depends on the table that should be processed, and if it does, it checks
if its pg_class(relrewrite) attribute points to the table to be processed. In
such a case it drops the new table because it evidently belongs to the
previous (failed) run.

Besides creation of the new table, separate transaction is also used to each
index,
---
 src/backend/commands/matview.c   |   2 +-
 src/backend/commands/repack.c    | 880 +++++++++++++++++++++++++------
 src/backend/commands/tablecmds.c |   2 +-
 src/include/commands/repack.h    |  10 +-
 src/tools/pgindent/typedefs.list |   2 +
 5 files changed, 743 insertions(+), 153 deletions(-)

diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..9d490da5f81 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -317,7 +317,7 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData,
 	 */
 	OIDNewHeap = make_new_heap(matviewOid, tableSpace,
 							   matviewRel->rd_rel->relam,
-							   relpersistence, ExclusiveLock);
+							   relpersistence, ExclusiveLock, false);
 	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
 
 	/* Generate the data, if wanted. */
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 1ad453a39a0..89695335127 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -51,6 +51,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_depend.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
@@ -96,6 +97,38 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * Information needed to close and re-open relation on transaction boundary.
+ */
+typedef struct RelReopenInfo
+{
+	Oid			relid;
+	Relation   *p_rel;
+	bool		is_new;
+} RelReopenInfo;
+
+/*
+ * Information needed to release ans re-initialize ChangeContext on
+ * transaction boundary.
+ */
+typedef struct ChangeContexBackup
+{
+	/* The new relation. */
+	Relation	rel;
+	RelReopenInfo rel_ri;
+	Oid			ident_index;
+
+	/* Auxiliary relation. */
+	Relation	rel_aux;
+	RelReopenInfo rel_aux_ri;
+	Oid			ident_index_aux;
+
+	/* Common fields */
+	int			file_seq_snapshot;
+	int			file_seq_changes;
+	Oid			clustering_index;
+} ChangeContextBackup;
+
 /*
  * When REPACK (CONCURRENTLY) copies data to the new heap, a new snapshot is
  * built after processing this many pages. XXX Tune the value.
@@ -120,6 +153,13 @@ typedef struct DecodingWorker
 /* Pointer to currently running decoding worker. */
 static DecodingWorker *decoding_worker = NULL;
 
+/*
+ * In the CONCURRENTLY mode, we need multiple transactions to process a single
+ * table. Information that needs to survive commit should be stored in this
+ * context.
+ */
+static MemoryContext repack_cxt = NULL;
+
 /*
  * Is there a message sent by a repack worker that the backend needs to
  * receive?
@@ -134,6 +174,14 @@ static void check_concurrent_repack_requirements(Relation rel,
 												 Oid *ident_idx_p);
 static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 							 Oid ident_idx);
+static ChangeContext *make_new_heap_for_repack(Oid tablespace,
+											   Oid access_method,
+											   char relpersistence,
+											   Oid ident_idx,
+											   Relation *p_old_heap,
+											   Relation *p_clustering_index,
+											   IndexBuildSecurity *ibsec);
+static List *find_new_heaps(Oid relid_old);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 							bool verbose,
 							bool *pSwapToastByContent,
@@ -175,15 +223,30 @@ static void release_change_context(ChangeContext *chgcxt);
 static void initialize_change_dest(RepackDest *dest, Relation relation,
 								   Oid ident_index_id);
 static void release_change_dest(RepackDest *dest);
+static void backup_change_context(ChangeContext *chgcxt,
+								  ChangeContextBackup *backup);
+static ChangeContext *reinitialize_change_context(ChangeContextBackup *backup);
+static void enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new);
+static void prepare_relation_for_reopening(Relation *p_rel,
+										   RelReopenInfo *backup,
+										   bool is_new);
+static void reopen_relation(RelReopenInfo *backup);
 static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 											   Oid identIdx,
 											   TransactionId frozenXid,
 											   MultiXactId cutoffMulti,
 											   ChangeContext *chgcxt);
-static void process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap,
-									Oid identIdx);
-static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
+static ChangeContext *process_auxiliary_table(ChangeContext *chgcxt,
+											  Relation *pOldHeap,
+											  Relation *pNewHeap,
+											  Oid identIdx);
+static List *build_new_indexes(List *OldIndexes, Relation *p_old,
+							   Relation *p_new,
+							   ChangeContext **p_chgcxt);
 static Oid	build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex);
+static ChangeContext *start_new_transaction(ChangeContext *chgcxt,
+											Relation *p_old, Relation *p_new);
 static void copy_index_constraints(Relation old_index, Oid new_index_id,
 								   Oid new_heap_id);
 static void copy_attribute_defaults(Oid old_heap_oid, Oid new_heap_oid);
@@ -518,7 +581,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
 	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
 	Oid			ident_idx = InvalidOid;
-	IndexBuildSecurity ibsec;
 
 	/* Determine the lock mode to use. */
 	lmode = RepackLockLevel(concurrent);
@@ -536,11 +598,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid);
 	pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd);
 
-	/*
-	 * Prevent index functions from doing what they are not supposed to.
-	 */
-	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
-
 	/*
 	 * Recheck that the relation is still what it was when we started.
 	 *
@@ -662,6 +719,17 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (concurrent)
 	{
+		/*
+		 * In the CONCURRENTLY mode, new transactions may be started. Make
+		 * sure we can preserve information across transaction boundaries.
+		 */
+		if (repack_cxt == NULL)
+			repack_cxt = AllocSetContextCreate(TopMemoryContext,
+											   "Repack - Top",
+											   ALLOCSET_DEFAULT_SIZES);
+		else
+			MemoryContextReset(repack_cxt);
+
 		PG_ENSURE_ERROR_CLEANUP(stop_repack_decoding_worker_cb, 0);
 		{
 			rebuild_relation(OldHeap, index, verbose, ident_idx);
@@ -673,9 +741,6 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		rebuild_relation(OldHeap, index, verbose, ident_idx);
 
 out:
-	/* Relax the restrictions imposed above. */
-	disable_index_build_security(&ibsec);
-
 	pgstat_progress_end_command();
 }
 
@@ -999,13 +1064,14 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
 	Oid			tableSpace = OldHeap->rd_rel->reltablespace;
-	Oid			OIDNewHeap;
-	Relation	NewHeap;
+	Oid			OIDNewHeap = InvalidOid;
+	Relation	NewHeap = NULL;
 	char		relpersistence;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
 	bool		concurrent = OidIsValid(ident_idx);
+	IndexBuildSecurity ibsec;
 	ChangeContext *chgcxt = NULL;
 #if USE_ASSERT_CHECKING
 	LOCKMODE	lmode;
@@ -1016,8 +1082,15 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
 #endif
 
+	/*
+	 * Prevent index functions from doing what they are not supposed to.
+	 */
+	enable_index_build_security(OldHeap->rd_rel->relowner, &ibsec);
+
 	if (concurrent)
 	{
+		MemoryContext oldcxt;
+
 		/*
 		 * The worker needs to be member of the locking group we're the leader
 		 * of. We ought to become the leader before the worker starts. The
@@ -1041,8 +1114,12 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 		 * Not sure this risk is worth unlocking/locking the table (and its
 		 * clustering index) and checking again if it's still eligible for
 		 * REPACK CONCURRENTLY.
+		 *
+		 * Use a transaction context that survives transaction commit(s).
 		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		start_repack_decoding_worker(tableOid);
+		MemoryContextSwitchTo(oldcxt);
 	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
@@ -1052,112 +1129,32 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
 
-	/*
-	 * Create the transient table that will receive the re-ordered data.
-	 *
-	 * OldHeap is already locked, so no need to lock it again.  make_new_heap
-	 * obtains AccessExclusiveLock on the new heap and its toast table.
-	 */
-	OIDNewHeap = make_new_heap(tableOid, tableSpace,
-							   accessMethod,
-							   relpersistence,
-							   NoLock);
-	Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false));
-	NewHeap = table_open(OIDNewHeap, NoLock);
-
 	if (concurrent)
 	{
-		bool		need_aux_rel;
-
 		/*
-		 * Auxiliary table is needed for clustering in the CONCURRENTLY mode,
-		 * see comments in ChangeContext. FIXME Non-btree indexes are allowed
-		 * historically, but in general, these can hardly define any useful
-		 * order. We ignore them here.
+		 * Create the transient table that will receive the re-ordered data,
+		 * and possibly the auxiliary table for the ordered data.
 		 */
-		need_aux_rel = index != NULL && index->rd_rel->relam == BTREE_AM_OID;
-
-		/* Gather information to apply concurrent changes. */
-		chgcxt = palloc0_object(ChangeContext);
+		chgcxt = make_new_heap_for_repack(tableSpace, accessMethod,
+										  relpersistence, ident_idx,
+										  &OldHeap, &index, &ibsec);
+		NewHeap = chgcxt->cc_dest.rel;
+	}
+	else
+	{
+		Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
 
 		/*
-		 * Create a copy of the attribute defaults on the temp table, which
-		 * the executor needs when replaying concurrent data changes.
+		 * Nothing special here. No locking as the caller should already have
+		 * a lock on the old relation, and the new one will be locked by
+		 * make_new_heap() anyway.
 		 */
-		copy_attribute_defaults(tableOid, OIDNewHeap);
-
-		if (!need_aux_rel)
-		{
-			Oid			ident_idx_new;
-
-			/*
-			 * Create the identity index. We will need it during data copying
-			 * so that we can apply the data changes at the appropriate time -
-			 * see comments around the call of
-			 * repack_process_concurrent_changes() with block range specified.
-			 *
-			 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
-			 */
-			ident_idx_new = build_new_index(NewHeap, OldHeap, ident_idx);
-
-			initialize_change_context(chgcxt, NewHeap, ident_idx_new);
-		}
-		else
-		{
-			Oid			aux_oid;
-			Relation	aux_rel;
-			Oid			aux_ident_idx;
-
-			/*
-			 * As the concurrent data changes will be applied to the auxiliary
-			 * heap, the new heap does not need the identity index yet. We'll
-			 * build it after having copied the data from the auxiliary heap.
-			 * (Bulk insert should be more efficient.)
-			 */
-			initialize_change_context(chgcxt, NewHeap, InvalidOid);
-
-			/*
-			 * Like above, but only temporary - no other backend should need
-			 * it.
-			 */
-			aux_oid = make_new_heap(tableOid, tableSpace, accessMethod,
-									RELPERSISTENCE_TEMP, NoLock);
-			Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
-											  false));
-			aux_rel = table_open(aux_oid, NoLock);
-
-
-			/*
-			 * Copy the attribute defaults as we did for the new heap above -
-			 * the concurrent changes also need to be applied to the auxiliary
-			 * table.
-			 */
-			copy_attribute_defaults(tableOid, aux_oid);
-
-			/*
-			 * The same for identity index. (The additional
-			 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll
-			 * be released at the end of transaction.)
-			 */
-			aux_ident_idx = build_new_index(aux_rel, OldHeap, ident_idx);
-
-			/*
-			 * Make the relation ready for use.
-			 */
-			chgcxt->cc_dest_aux = palloc0_object(RepackDest);
-			initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
-								   aux_ident_idx);
-
-			/*
-			 * Set OID of the old relation's clustering index if it's
-			 * different from the identity index. Otherwise set InvalidOid to
-			 * indicate that the identity index should be used for clustering.
-			 */
-			if (RelationGetRelid(index) != ident_idx)
-				chgcxt->cc_clustering_index = RelationGetRelid(index);
-			else
-				chgcxt->cc_clustering_index = InvalidOid;
-		}
+		OIDNewHeap = make_new_heap(tableOid, tableSpace, accessMethod,
+								   relpersistence, NoLock, false);
+		/* No additional lock needed. */
+		Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock,
+										  false));
+		NewHeap = table_open(OIDNewHeap, NoLock);
 	}
 
 	/* Copy the heap data into the new table in the desired order */
@@ -1221,6 +1218,335 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
 						 frozenXid, cutoffMulti,
 						 relpersistence);
 	}
+
+	/* Relax the restrictions imposed above. */
+	disable_index_build_security(&ibsec);
+}
+
+/*
+ * Wrapper around make_new_heap() to handle specifics of REPACK
+ * (CONCURRENTLY).
+ *
+ * 'ident_idx' is the identity index, to handle replaying of concurrent data
+ * changes to the new heap.
+ *
+ * The function starts a new transaction. Therefore, both old heap and
+ * clustering index are passed in the form of pointer so the caller has the
+ * correct entries in the new transaction. New heap is returned in the
+ * ChangeContext object. On return, the new heap will be locked in
+ * AccessExclusiveLock mode.
+ */
+static ChangeContext *
+make_new_heap_for_repack(Oid tablespace, Oid access_method,
+						 char relpersistence, Oid ident_idx,
+						 Relation *p_old_heap, Relation *p_clustering_index,
+						 IndexBuildSecurity *ibsec)
+{
+	Relation	old_heap = *p_old_heap;
+	Relation	clustering_index = *p_clustering_index;
+	List	   *existing = NIL;
+	Relation	new_heap;
+	Oid			old_heap_oid = RelationGetRelid(old_heap);
+	bool		need_aux_rel;
+	Oid			new_heap_oid;
+	ObjectAddress old_obj,
+				new_obj;
+	Oid			ident_idx_new;
+	Oid			aux_oid = InvalidOid;
+	Oid			aux_ident_idx = InvalidOid;
+	Oid			clustering_index_oid = InvalidOid;
+	Relation	aux_rel = NULL;
+	ChangeContext *chgcxt;
+
+	/*
+	 * In the CONCURRENTLY case, we do the catalog changes in a separate
+	 * transaction so that we do not have XID assigned while copying the data.
+	 * (That XID would block the progress of the xmin horizon for VACUUM.)
+	 * Therefore, the new relation will be created in a separate transaction
+	 * and thus be visible to other transactions, although our lock should not
+	 * let them access it. However, if previous run of REPACK ended with
+	 * ERROR, such relation may still be there.
+	 *
+	 * Besides that, an auxiliary relation (for tuple sorting) might exist.
+	 * Thus we expect a list of zero, one or two items.
+	 *
+	 * Since the relations should depend on the old one (see below), we use
+	 * the pg_depend catalog to find it.
+	 */
+	existing = find_new_heaps(old_heap_oid);
+	Assert(list_length(existing) <= 2);
+	foreach_oid(existing_oid, existing)
+	{
+		/* There appears to be one, so simply drop it. */
+		new_obj.classId = RelationRelationId;
+		new_obj.objectId = existing_oid;
+		new_obj.objectSubId = InvalidOid;
+
+		/*
+		 * User is not supposed to create any dependencies on the relation, so
+		 * CASCADE.
+		 */
+		performDeletion(&new_obj, DROP_CASCADE, PERFORM_DELETION_INTERNAL);
+	}
+
+	/*
+	 * The old heap should already be locked by the caller. As for the new
+	 * one, lock is needed because the commit below will make it visible to
+	 * other transactions. make_new_heap() should lock it.
+	 */
+	Assert(CheckRelationLockedByMe(old_heap, ShareUpdateExclusiveLock,
+								   false));
+	new_heap_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								 relpersistence, NoLock, false);
+	Assert(CheckRelationOidLockedByMe(new_heap_oid, AccessExclusiveLock,
+									  false));
+
+	new_heap = table_open(new_heap_oid, NoLock);
+
+	/*
+	 * Create a copy of the attribute defaults on the temp table, which the
+	 * executor needs when replaying concurrent data changes.
+	 */
+	copy_attribute_defaults(old_heap_oid, new_heap_oid);
+
+	/*
+	 * Auxiliary table is needed for clustering in the CONCURRENTLY mode, see
+	 * comments in ChangeContext. FIXME Non-btree indexes are allowed
+	 * historically, but in general, these can hardly define any useful order.
+	 * We ignore them here.
+	 */
+	need_aux_rel = clustering_index != NULL &&
+		clustering_index->rd_rel->relam == BTREE_AM_OID;
+	if (!need_aux_rel)
+	{
+		/*
+		 * Create the identity index. We will need it during data copying so
+		 * that we can apply the data changes at the appropriate time - see
+		 * comments around the call of repack_process_concurrent_changes()
+		 * with block range specified.
+		 *
+		 * XXX NewHeap is empty - should we pass INDEX_CREATE_SKIP_BUILD?
+		 */
+		ident_idx_new = build_new_index(new_heap, old_heap, ident_idx);
+	}
+	else
+	{
+		/*
+		 * As the concurrent data changes will be applied to the auxiliary
+		 * heap, the new heap does not need the identity index yet. We'll
+		 * build it after having copied the data from the auxiliary heap.
+		 * (Bulk insert should be more efficient.)
+		 */
+		ident_idx_new = InvalidOid;
+
+		/*
+		 * Create the auxiliary table - like the new heap above, but only
+		 * unlogged - neither crash recovery nor replication is needed.
+		 */
+		aux_oid = make_new_heap(old_heap_oid, tablespace, access_method,
+								RELPERSISTENCE_UNLOGGED, NoLock, true);
+		Assert(CheckRelationOidLockedByMe(aux_oid, AccessExclusiveLock,
+										  false));
+
+		/*
+		 * The same for identity index. (The additional
+		 * ShareUpdateExclusiveLock on ident_idx is not a problem, it'll be
+		 * released at the end of transaction.)
+		 */
+		aux_rel = table_open(aux_oid, NoLock);
+		aux_ident_idx = build_new_index(aux_rel, old_heap, ident_idx);
+
+		clustering_index_oid = RelationGetRelid(clustering_index);
+
+		/*
+		 * Copy the attribute defaults as we did for the new heap above - the
+		 * concurrent changes also need to be applied to the auxiliary table.
+		 */
+		copy_attribute_defaults(old_heap_oid, aux_oid);
+	}
+
+	/*
+	 * Create dependencies on the old heap - it ensures that dropping the old
+	 * heap automatically cascades to the new ones.
+	 */
+	old_obj.classId = RelationRelationId;
+	old_obj.objectId = old_heap_oid;
+	old_obj.objectSubId = InvalidOid;
+	new_obj.classId = RelationRelationId;
+	new_obj.objectId = new_heap_oid;
+	new_obj.objectSubId = InvalidOid;
+	recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+
+	if (OidIsValid(aux_oid))
+	{
+		new_obj.objectId = aux_oid;
+		recordDependencyOn(&new_obj, &old_obj, DEPENDENCY_AUTO);
+	}
+
+	/*
+	 * Commit will release the locks, so make sure no one can alter or drop
+	 * the relations until we're done.
+	 */
+	enable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+	/* NoLock for simplicity, commit will release the existing lock anyway. */
+	table_close(old_heap, NoLock);
+
+	/* Likewise, acquire session lock for the new heap. */
+	enable_session_locks(new_heap, AccessExclusiveLock, true);
+	table_close(new_heap, NoLock);
+
+	/*
+	 * The same for the auxiliary relation and clustering index if ordering is
+	 * required.
+	 */
+	if (need_aux_rel)
+	{
+		enable_session_locks(aux_rel, AccessExclusiveLock, true);
+		table_close(aux_rel, NoLock);
+
+		/* is_new does not matter for index. */
+		enable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							 false);
+		index_close(clustering_index, NoLock);
+	}
+
+	/*
+	 * Even if the clustering index is not appropriate for sorting, we ought
+	 * to close it before committing the transaction.
+	 */
+	else if (clustering_index)
+		index_close(clustering_index, NoLock);
+
+	/*
+	 * Commit the current transaction and start a new one.
+	 */
+	disable_index_build_security(ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec->userid, ibsec);
+
+	/*
+	 * Open the new heap in the new transaction and disable the session
+	 * lock(s).
+	 */
+	new_heap = table_open(new_heap_oid, NoLock);
+	disable_session_locks(new_heap, AccessExclusiveLock, true);
+
+	/*
+	 * Allocate and initialize memory for the output information. We shouldn't
+	 * have done it in the memory context of the previous transaction.
+	 */
+	chgcxt = palloc0_object(ChangeContext);
+	initialize_change_context(chgcxt, new_heap, ident_idx_new);
+	chgcxt->cc_ind_build_sec = *ibsec;
+	if (need_aux_rel)
+	{
+		/*
+		 * Make the auxiliary relation ready for use.
+		 */
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		aux_rel = table_open(aux_oid, NoLock);
+		disable_session_locks(aux_rel, AccessExclusiveLock, true);
+		initialize_change_dest(chgcxt->cc_dest_aux, aux_rel,
+							   aux_ident_idx);
+
+		/*
+		 * Set OID of the old relation's clustering index if it's different
+		 * from the identity index. Otherwise set InvalidOid to indicate that
+		 * the identity index should be used for clustering.
+		 */
+		if (clustering_index_oid != ident_idx)
+			chgcxt->cc_clustering_index = clustering_index_oid;
+		else
+			chgcxt->cc_clustering_index = InvalidOid;
+
+		/* Re-open the clustering index. */
+		clustering_index = index_open(clustering_index_oid, NoLock);
+		/* is_new does not matter for index. */
+		disable_session_locks(clustering_index, ShareUpdateExclusiveLock,
+							  false);
+	}
+	else
+		clustering_index = NULL;
+
+	/*
+	 * Re-open the old heap.
+	 */
+	old_heap = table_open(old_heap_oid, NoLock);
+	disable_session_locks(old_heap, ShareUpdateExclusiveLock, false);
+
+	*p_old_heap = old_heap;
+	*p_clustering_index = clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Check if new heap, and possibly also an auxiliary heap, already exists for
+ * given old heap - typically due to failed REPACK (CONCURRENTLY). Return OIDs
+ * of in a list or NIL if there is none.
+ */
+static List *
+find_new_heaps(Oid relid_old)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				RelationRelationId);
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid_old));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(InvalidOid));
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_depend depform;
+
+		/*
+		 * There should be AUTO dependency from another table, which is the
+		 * new or the auxiliary one. If we found any other, just ignore them.
+		 */
+		depform = (Form_pg_depend) GETSTRUCT(tup);
+		if (depform->classid == RelationRelationId &&
+			depform->deptype == DEPENDENCY_AUTO)
+		{
+			Relation	rel;
+			Form_pg_class classForm;
+			Oid			matched = InvalidOid;
+
+			rel = relation_open(depform->objid, AccessShareLock);
+
+			/*
+			 * The new relation's relrewrite should point to the old relation.
+			 */
+			classForm = rel->rd_rel;
+			if (classForm->relrewrite == relid_old)
+				matched = depform->objid;
+			table_close(rel, AccessShareLock);
+
+			if (OidIsValid(matched))
+				result = lappend_oid(result, matched);
+		}
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
 }
 
 /*
@@ -1235,7 +1561,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose,
  */
 Oid
 make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-			  char relpersistence, LOCKMODE lockmode)
+			  char relpersistence, LOCKMODE lockmode, bool auxiliary)
 {
 	TupleDesc	OldHeapDesc;
 	char		NewHeapName[NAMEDATALEN];
@@ -1246,6 +1572,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	Datum		reloptions;
 	bool		isNull;
 	Oid			namespaceid;
+	const char *suffix;
 
 	OldHeap = table_open(OIDOldHeap, lockmode);
 	OldHeapDesc = RelationGetDescr(OldHeap);
@@ -1285,7 +1612,9 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 	 * mapped.  This simplifies swap_relation_files, and is absolutely
 	 * necessary for rebuilding pg_class, for reasons explained there.
 	 */
-	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap);
+	suffix = auxiliary ? "_aux" : "";
+	snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u%s", OIDOldHeap,
+			 suffix);
 
 	OIDNewHeap = heap_create_with_catalog(NewHeapName,
 										  namespaceid,
@@ -2562,31 +2891,6 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	if (params->options & CLUOPT_CONCURRENT)
-	{
-		/*
-		 * Since REPACK (CONCURRENTLY) pops the active snapshot during the
-		 * processing (it creates and pushes snapshots on its own), and since
-		 * that snapshot can be referenced by the current portal, we need to
-		 * make sure that the portal has no dangling pointer to the snapshot.
-		 * Starting a new transaction seems to be the simplest way.
-		 *
-		 * XXX The following patches in the series make this unnecessary, as
-		 * they start new transactions for other reasons elsewhere.
-		 */
-		PopActiveSnapshot();
-		CommitTransactionCommand();
-
-		/* Start a new transaction. */
-		StartTransactionCommand();
-
-		/*
-		 * Functions in indexes may want a snapshot set. Note that the portal
-		 * is not aware of this one, so the caller needs to pop it explicitly.
-		 */
-		PushActiveSnapshot(GetTransactionSnapshot());
-	}
-
 	/*
 	 * Make sure ANALYZE is specified if a column list is present.
 	 */
@@ -3476,6 +3780,176 @@ release_change_dest(RepackDest *dest)
 	pfree(dest->ident_key);
 }
 
+/*
+ * Copy information needed to re-initialize 'chgcxt' to 'backup'.
+ */
+static void
+backup_change_context(ChangeContext *chgcxt, ChangeContextBackup *backup)
+{
+	RepackDest *dest;
+
+	memset(backup, 0, sizeof(ChangeContextBackup));
+
+	/* Store information on the new relation. */
+	dest = &chgcxt->cc_dest;
+	backup->rel = dest->rel;
+	prepare_relation_for_reopening(&backup->rel, &backup->rel_ri, true);
+	backup->ident_index = dest->ident_index ?
+		RelationGetRelid(dest->ident_index) : InvalidOid;
+
+	/* Store information on the auxiliary relation if one exists. */
+	if (chgcxt->cc_dest_aux)
+	{
+		dest = chgcxt->cc_dest_aux;
+		backup->rel_aux = dest->rel;
+		prepare_relation_for_reopening(&backup->rel_aux, &backup->rel_aux_ri,
+									   true);
+		backup->ident_index_aux = dest->ident_index ?
+			RelationGetRelid(dest->ident_index) : InvalidOid;
+	}
+	else
+	{
+		/* There should be no reopening. */
+		Assert(backup->rel_aux_ri.relid == InvalidOid);
+	}
+
+	/* Backup the common fields. */
+	backup->file_seq_snapshot = chgcxt->cc_file_seq_snapshot;
+	backup->file_seq_changes = chgcxt->cc_file_seq_changes;
+	backup->clustering_index = chgcxt->cc_clustering_index;
+}
+
+/*
+ * Create and initialize an instance of ChangeContext, based on previously
+ * existing instance. It includes re-opening of relations and indexes.
+ */
+static ChangeContext *
+reinitialize_change_context(ChangeContextBackup *backup)
+{
+	ChangeContext *chgcxt;
+	RelReopenInfo *rri;
+
+	chgcxt = palloc0_object(ChangeContext);
+
+	/* Re-initialize the new relation part. */
+	rri = &backup->rel_ri;
+	reopen_relation(rri);
+	initialize_change_context(chgcxt, backup->rel, backup->ident_index);
+
+	/* The same for the auxiliary relation, if it exists. */
+	rri = &backup->rel_aux_ri;
+	if (OidIsValid(rri->relid))
+	{
+		reopen_relation(rri);
+		chgcxt->cc_dest_aux = palloc0_object(RepackDest);
+		initialize_change_dest(chgcxt->cc_dest_aux, backup->rel_aux,
+							   backup->ident_index_aux);
+	}
+
+	/* Restore the common fields. */
+	chgcxt->cc_file_seq_snapshot = backup->file_seq_snapshot;
+	chgcxt->cc_file_seq_changes = backup->file_seq_changes;
+	chgcxt->cc_clustering_index = backup->clustering_index;
+
+	return chgcxt;
+}
+
+/*
+ * Lock the relation and its TOAST relation using a session lock.
+ */
+static void
+enable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+	lock = rel->rd_lockInfo.lockRelId;
+	LockRelationIdForSession(&lock, lmode);
+
+	/*
+	 * The same for TOAST relation. XXX Is it ok that we do not lock TOAST
+	 * relation of the old relation until we start swapping the files? (The
+	 * new relation's TOAST is locked on creation, so we keep it that way.)
+	 */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+		lock.relId = toastid;
+		LockRelationIdForSession(&lock, lmode);
+	}
+}
+
+/*
+ * Disable session locks acquired earlier by enable_session_locks(), but first
+ * acquire normal (transactional) lock(s).
+ */
+static void
+disable_session_locks(Relation rel, LOCKMODE lmode, bool is_new)
+{
+	LockRelId	lock;
+	Oid			toastid;
+
+	LockRelation(rel, lmode);
+	lock = rel->rd_lockInfo.lockRelId;
+	UnlockRelationIdForSession(&lock, lmode);
+	Assert(CheckRelationLockedByMe(rel, lmode, false));
+
+	/* The same for TOAST relation. See enable_session_locks(). */
+	toastid = rel->rd_rel->reltoastrelid;
+	if (is_new && OidIsValid(toastid))
+	{
+		lock.relId = toastid;
+		LockRelationId(&lock, lmode);
+		UnlockRelationIdForSession(&lock, lmode);
+		Assert(CheckRelationOidLockedByMe(toastid, lmode, false));
+	}
+}
+
+/*
+ * Remember OID of a relation, lock it using a session lock and close it. and a
+ *
+ * The lock mode is always ShareUpdateExclusiveLock, as this is related to
+ * REPACK (CONCURRENTLY).
+ */
+static void
+prepare_relation_for_reopening(Relation *p_rel, RelReopenInfo *backup,
+							   bool is_new)
+{
+	Relation	rel = *p_rel;
+	LOCKMODE	lmode;
+
+	lmode = is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+
+	/* Remember what we need to be able to re-open the relation later. */
+	backup->relid = RelationGetRelid(rel);
+	backup->p_rel = p_rel;
+	backup->is_new = is_new;
+
+	/*
+	 * Close it, but make sure the lock is not lost at transaction boundary.
+	 */
+	enable_session_locks(rel, lmode, is_new);
+	relation_close(rel, NoLock);
+}
+
+/*
+ * Open the relation, acquire transactional lock on it and release the session
+ * lock acquired by prepare_relation_for_reopening().
+ */
+static void
+reopen_relation(RelReopenInfo *backup)
+{
+	Relation	rel;
+	LOCKMODE	lmode;
+
+	lmode = backup->is_new ? AccessExclusiveLock : ShareUpdateExclusiveLock;
+	rel = relation_open(backup->relid, lmode);
+	disable_session_locks(rel, lmode, backup->is_new);
+	*backup->p_rel = rel;
+}
+
 /*
  * The final steps of rebuild_relation() for concurrent processing.
  *
@@ -3493,21 +3967,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	List	   *ind_oids_new;
 	Oid			old_table_oid = RelationGetRelid(OldHeap);
 	Oid			new_table_oid = RelationGetRelid(NewHeap);
-	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	List	   *ind_oids_old;
 	ListCell   *lc,
 			   *lc2;
 	char		relpersistence;
 	bool		is_system_catalog;
 	XLogRecPtr	end_of_wal;
+	MemoryContext oldcxt;
 	List	   *indexrels;
 	List	   *inds_tmp = NIL;
 
 	Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false));
 	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
 
-	/* If we have the auxiliary table, this is the moment we should use it. */
+	/*
+	 * If we have the auxiliary table, this is the moment we should use it.
+	 * Note that the function can start new transaction(s). In that case it
+	 * re-opens the new and old heap, so pass pointers to get the relation
+	 * cache entries updated.
+	 */
 	if (chgcxt->cc_dest_aux)
-		process_auxiliary_table(chgcxt, OldHeap, identIdx);
+		chgcxt = process_auxiliary_table(chgcxt, &OldHeap, &NewHeap, identIdx);
 
 	/*
 	 * Unlike the exclusive case, we build new indexes for the new relation
@@ -3530,13 +4010,17 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 	 * auxiliary table) can be a problem in terms of index layout. Shouldn't
 	 * we drop the identity index and build it using bulk insert too?
 	 */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	ind_oids_old = RelationGetIndexList(OldHeap);
 	foreach_oid(ind_oid, ind_oids_old)
 	{
 		if (ind_oid != identIdx)
 			inds_tmp = lappend_oid(inds_tmp, ind_oid);
 	}
+	MemoryContextSwitchTo(oldcxt);
 	ind_oids_old = inds_tmp;
-	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+	ind_oids_new = build_new_indexes(ind_oids_old, &OldHeap, &NewHeap,
+									 &chgcxt);
 
 	/*
 	 * The identity index will be involved in the following processing.
@@ -3695,9 +4179,15 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
 /*
  * Copy the contents of the auxiliary table to the new table in the desired
  * order, then drop the auxiliary table.
+ *
+ * All the opened relations may need to be closed and re-opened because the
+ * function may start a new transaction. That's why a pointer to old and new
+ * heap is passed. 'chgcxt' is re-created in that case, so user should use the
+ * returned value.
  */
-static void
-process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
+static ChangeContext *
+process_auxiliary_table(ChangeContext *chgcxt, Relation *pOldHeap,
+						Relation *pNewHeap, Oid identIdx)
 {
 	RepackDest *dest = chgcxt->cc_dest_aux;
 	Oid			ident_idx_new;
@@ -3718,8 +4208,16 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 		/*
 		 * Create it according to the clustering index on the old relation.
 		 */
-		cl_ind_oid = build_new_index(dest->rel, OldHeap,
+		cl_ind_oid = build_new_index(dest->rel, *pOldHeap,
 									 chgcxt->cc_clustering_index);
+
+		/*
+		 * The index build generated a new XID, so commit the transaction and
+		 * start a new one.
+		 */
+		chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
+		dest = chgcxt->cc_dest_aux;
+
 		clustering_index = index_open(cl_ind_oid, NoLock);
 	}
 	else
@@ -3778,7 +4276,13 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
 
 	/* Build the identity index on the new relation. */
-	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, OldHeap, identIdx);
+	ident_idx_new = build_new_index(chgcxt->cc_dest.rel, *pOldHeap, identIdx);
+
+	/*
+	 * The index build generated a new XID, so commit the transaction and
+	 * start a new one.
+	 */
+	chgcxt = start_new_transaction(chgcxt, pOldHeap, pNewHeap);
 
 	/*
 	 * Make the new heap ready to use the index for future replaying of
@@ -3787,6 +4291,8 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
 	rel = chgcxt->cc_dest.rel;
 	release_change_dest(&chgcxt->cc_dest);
 	initialize_change_dest(&chgcxt->cc_dest, rel, ident_idx_new);
+
+	return chgcxt;
 }
 
 /*
@@ -3798,20 +4304,47 @@ process_auxiliary_table(ChangeContext *chgcxt, Relation OldHeap, Oid identIdx)
  * A list of OIDs of the corresponding indexes created on NewHeap is
  * returned. The order of items does match, so we can use these arrays to swap
  * index storage.
+ *
+ * A separate transaction is used for each index. Therefore caller needs to
+ * pass pointers to the relcache entries so that we can provide him with new
+ * entries after reopening the relations. Likewise, pointer to ChangeContext
+ * is used to return the re-created instance.
  */
 static List *
-build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+build_new_indexes(List *OldIndexes, Relation *p_old, Relation *p_new,
+				  ChangeContext **p_chgcxt)
 {
+	ChangeContext *chgcxt = *p_chgcxt;
 	List	   *result = NIL;
 
 	foreach_oid(oldindex, OldIndexes)
 	{
+		Relation	rel_old = *p_old;
+		Relation	rel_new = *p_new;
 		Oid			newindex;
+		MemoryContext oldcxt;
+
+		newindex = build_new_index(rel_new, rel_old, oldindex);
 
-		newindex = build_new_index(NewHeap, OldHeap, oldindex);
+		/*
+		 * Allocate the list in repack_cxt so it survives the current
+		 * transaction.
+		 */
+		oldcxt = MemoryContextSwitchTo(repack_cxt);
 		result = lappend_oid(result, newindex);
+		MemoryContextSwitchTo(oldcxt);
+
+		/*
+		 * Use one transaction per index, to limit the impact on xmin
+		 * horizons. XXX Does it make sense to even first commit the catalog
+		 * changes and then do the build in a new transaction (which should
+		 * not set MyProc->xmin until the build is complete)? Not sure.
+		 */
+		chgcxt = start_new_transaction(chgcxt, p_old, p_new);
 	}
 
+	*p_chgcxt = chgcxt;
+
 	return result;
 }
 
@@ -3835,17 +4368,62 @@ build_new_index(Relation NewHeap, Relation OldHeap, Oid oldindex)
 								 "repacknew",
 								 get_rel_namespace(ind->rd_index->indrelid),
 								 false);
+
 	/* Functions in indexes may want a snapshot set. */
 	Assert(ActiveSnapshotSet());
 	newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS,
 								 oldindex, ind->rd_rel->reltablespace,
 								 newName);
+
 	copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap));
 	index_close(ind, NoLock);
 
 	return newindex;
 }
 
+/*
+ * Start a new transaction, but make sure that the caller still has valid
+ * relcache entries as well as valid ChangeContext. The problem is that
+ * relcache references should be closed before transaction commit, so we need
+ * them re-opened in the new transaction. Also, commit releases all locks
+ * acquired in the transaction, however we don't want to lose our locks. We
+ * work it around by using session locks temporarily.
+ */
+static ChangeContext *
+start_new_transaction(ChangeContext *chgcxt, Relation *p_old, Relation *p_new)
+{
+	ChangeContextBackup chgcxt_backup;
+	RelReopenInfo rri_old;
+	MemoryContext oldcxt;
+	IndexBuildSecurity ibsec = chgcxt->cc_ind_build_sec;
+
+	/* This also closes the new relation. */
+	backup_change_context(chgcxt, &chgcxt_backup);
+	release_change_context(chgcxt);
+	prepare_relation_for_reopening(p_old, &rri_old, false);
+
+	disable_index_build_security(&ibsec);
+	PopActiveSnapshot();
+	CommitTransactionCommand();
+	StartTransactionCommand();
+	PushActiveSnapshot(GetTransactionSnapshot());
+	enable_index_build_security(ibsec.userid, &ibsec);
+
+	/* Create 'chgcxt' using re-opened relations. */
+	oldcxt = MemoryContextSwitchTo(repack_cxt);
+	/* This also re-opens the new relation. */
+	chgcxt = reinitialize_change_context(&chgcxt_backup);
+	chgcxt->cc_ind_build_sec = ibsec;
+	MemoryContextSwitchTo(oldcxt);
+
+	reopen_relation(&rri_old);
+
+	/* Update the callers relcache entry for the new relation. */
+	*p_new = chgcxt->cc_dest.rel;
+
+	return chgcxt;
+}
+
 /*
  * Create a transient copy of a constraint -- supported by a transient
  * copy of the index that supports the original constraint.
@@ -4031,6 +4609,8 @@ start_repack_decoding_worker(Oid relid)
 	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
 		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
 	decoding_worker->seg = dsm_create(size, 0);
+	/* The segment should not be detached at transaction boundary. */
+	dsm_pin_mapping(decoding_worker->seg);
 
 	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
 	shared->initialized = false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d691b317011..427fe733153 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6099,7 +6099,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * unlogged anyway.
 			 */
 			OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
-									   persistence, lockmode);
+									   persistence, lockmode, false);
 
 			/*
 			 * Copy the heap data into the new table with the desired
diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h
index 8af73f8c81f..ad8125790b0 100644
--- a/src/include/commands/repack.h
+++ b/src/include/commands/repack.h
@@ -18,6 +18,7 @@
 #include "access/hio.h"
 #include "access/skey.h"
 #include "access/xlogdefs.h"
+#include "catalog/index.h"
 #include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
@@ -120,6 +121,12 @@ typedef struct ChangeContext
 	 * The index that defines ordering of the old table.
 	 */
 	Oid			cc_clustering_index;
+
+	/*
+	 * Information needed to disable / enable security restrictions on index
+	 * functions. This is needed when starting a new transaction.
+	 */
+	IndexBuildSecurity cc_ind_build_sec;
 } ChangeContext;
 
 extern PGDLLIMPORT int repack_pages_per_snapshot;
@@ -133,7 +140,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
 extern Oid	make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
-						  char relpersistence, LOCKMODE lockmode);
+						  char relpersistence, LOCKMODE lockmode,
+						  bool auxiliary);
 extern void heap_insert_for_repack(ChangeContext *chgcxt, TupleTableSlot *src,
 								   TupleTableSlot *reform);
 extern bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0ec534d15d7..5a7e10cee9c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -431,6 +431,7 @@ CatalogId
 CatalogIdMapEntry
 CatalogIndexState
 ChangeContext
+ChangeContextBackup
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -2611,6 +2612,7 @@ RelMapping
 RelOptInfo
 RelOptKind
 RelPathStr
+RelReopenInfo
 RelStatsInfo
 RelSyncCallbackFunction
 RelToCheck
-- 
2.52.0


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v02-0007-Decouple-updating-of-freezing-information-from-swap_.patch



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


end of thread, other threads:[~2026-07-15 08:37 UTC | newest]

Thread overview: 144+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-05-18 15:20 [PATCH v16 2/2] handle relation statistics correctly during rewrites Bertrand Drouvot <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-06-16 11:54 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>
2026-07-15 08:37 [PATCH 6/8] Use separate transactions for catalog changes. Antonin Houska <[email protected]>

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