agora inbox for [email protected]
help / color / mirror / Atom feed[PATCH v11 1/2] Key PGSTAT_KIND_RELATION by relfile locator
160+ messages / 2 participants
[nested] [flat]
* [PATCH v11 1/2] Key PGSTAT_KIND_RELATION by relfile locator
@ 2025-10-01 09:45 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 160+ messages in thread
From: Bertrand Drouvot @ 2025-10-01 09:45 UTC (permalink / raw)
This patch changes the key used for the PGSTAT_KIND_RELATION statistic kind.
Instead of the relation oid, it now relies on:
- dboid (linked to RelFileLocator's dbOid)
- objoid which is the result of a new macro (namely RelFileLocatorToPgStatObjid())
that computes an objoid based on the RelFileLocator's spcOid and the
RelFileLocator's relNumber.
This is possible as, since b14e9ce7d55c, the objoid is now uint64 and spcOid
and relNumber are 32 bits.
That will allow us to add new stats (add writes counters) and ensure that some
counters (n_dead_tup and friends) are replicated.
The patch introduces pgstat_reloid_to_relfilelocator() to 1) avoid calling
RelationIdGetRelation() to get the relfilelocator based on the relation oid
and 2) handle the partitioned table case.
Please note that:
- when running pg_stat_have_stats('relation',...) we now need to be connected
to the database that hosts the relation. As pg_stat_have_stats() is not
documented publicly, then the changes done in 029_stats_restart.pl look
enough.
- this patch does not handle rewrites so some tests are failing. It's only
intent is to ease the review and should not be pushed without being
merged with the following patch that handles the rewrites.
- it can be used to test that stats are incremented correctly and that we're
able to retrieve them as long as rewrites are not involved.
---
src/backend/postmaster/autovacuum.c | 17 +-
src/backend/utils/activity/pgstat_relation.c | 236 ++++++++++++++++---
src/backend/utils/adt/pgstatfuncs.c | 22 +-
src/include/catalog/pg_tablespace.dat | 4 +
src/include/catalog/pg_tablespace.h | 8 +
src/include/pgstat.h | 15 +-
src/include/utils/pgstat_internal.h | 1 +
src/test/recovery/t/029_stats_restart.pl | 40 ++--
8 files changed, 271 insertions(+), 72 deletions(-)
6.1% src/backend/postmaster/
61.6% src/backend/utils/activity/
5.1% src/backend/utils/adt/
3.2% src/include/catalog/
5.6% src/include/
18.1% src/test/recovery/t/
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 6fde740465f..4bd5a6824bd 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1990,12 +1990,16 @@ do_autovacuum(void)
bool dovacuum;
bool doanalyze;
bool wraparound;
+ RelFileLocator locator;
if (classForm->relkind != RELKIND_RELATION &&
classForm->relkind != RELKIND_MATVIEW)
continue;
relid = classForm->oid;
+ locator.dbOid = classForm->relisshared ? InvalidOid : MyDatabaseId;
+ locator.spcOid = classForm->reltablespace;
+ locator.relNumber = classForm->relfilenode;
/*
* Check if it is a temp table (presumably, of some other backend's).
@@ -2024,8 +2028,7 @@ do_autovacuum(void)
/* Fetch reloptions and the pgstat entry for this table */
relopts = extract_autovac_opts(tuple, pg_class_desc);
- tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
- relid);
+ tabentry = pgstat_fetch_stat_tabentry_by_locator(locator);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
@@ -2090,6 +2093,7 @@ do_autovacuum(void)
bool dovacuum;
bool doanalyze;
bool wraparound;
+ RelFileLocator locator;
/*
* We cannot safely process other backends' temp tables, so skip 'em.
@@ -2098,6 +2102,9 @@ do_autovacuum(void)
continue;
relid = classForm->oid;
+ locator.dbOid = classForm->relisshared ? InvalidOid : MyDatabaseId;
+ locator.spcOid = classForm->reltablespace;
+ locator.relNumber = classForm->relfilenode;
/*
* fetch reloptions -- if this toast table does not have them, try the
@@ -2117,8 +2124,7 @@ do_autovacuum(void)
}
/* Fetch the pgstat entry for this table */
- tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
- relid);
+ tabentry = pgstat_fetch_stat_tabentry_by_locator(locator);
relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
effective_multixact_freeze_max_age,
@@ -2915,8 +2921,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
PgStat_StatTabEntry *tabentry;
/* fetch the pgstat table entry */
- tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
- relid);
+ tabentry = pgstat_fetch_stat_tabentry_ext(relid);
relation_needs_vacanalyze(relid, avopts, classForm, tabentry,
effective_multixact_freeze_max_age,
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..89bf0cbed56 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,17 @@
#include "postgres.h"
+#include "access/htup_details.h"
#include "access/twophase_rmgr.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/pg_tablespace.h"
+#include "storage/lmgr.h"
#include "utils/memutils.h"
#include "utils/pgstat_internal.h"
#include "utils/rel.h"
+#include "utils/relmapper.h"
+#include "utils/syscache.h"
#include "utils/timestamp.h"
@@ -36,13 +41,12 @@ typedef struct TwoPhasePgStatRecord
PgStat_Counter inserted_pre_truncdrop;
PgStat_Counter updated_pre_truncdrop;
PgStat_Counter deleted_pre_truncdrop;
- Oid id; /* table's OID */
- bool shared; /* is it a shared catalog? */
+ RelFileLocator locator; /* table's rd_locator */
bool truncdropped; /* was the relation truncated/dropped? */
} TwoPhasePgStatRecord;
-static PgStat_TableStatus *pgstat_prep_relation_pending(Oid rel_id, bool isshared);
+static PgStat_TableStatus *pgstat_prep_relation_pending(RelFileLocator locator);
static void add_tabstat_xact_level(PgStat_TableStatus *pgstat_info, int nest_level);
static void ensure_tabstat_xact_level(PgStat_TableStatus *pgstat_info);
static void save_truncdrop_counters(PgStat_TableXactStatus *trans, bool is_drop);
@@ -60,8 +64,7 @@ pgstat_copy_relation_stats(Relation dst, Relation src)
PgStatShared_Relation *dstshstats;
PgStat_EntryRef *dst_ref;
- srcstats = pgstat_fetch_stat_tabentry_ext(src->rd_rel->relisshared,
- RelationGetRelid(src));
+ srcstats = pgstat_fetch_stat_tabentry_ext(RelationGetRelid(src));
if (!srcstats)
return;
@@ -94,8 +97,10 @@ pgstat_init_relation(Relation rel)
/*
* We only count stats for relations with storage and partitioned tables
+ * and we don't count stats generated during a rewrite.
*/
- if (!RELKIND_HAS_STORAGE(relkind) && relkind != RELKIND_PARTITIONED_TABLE)
+ if ((!RELKIND_HAS_STORAGE(relkind) && relkind != RELKIND_PARTITIONED_TABLE) ||
+ OidIsValid(rel->rd_rel->relrewrite))
{
rel->pgstat_enabled = false;
rel->pgstat_info = NULL;
@@ -130,12 +135,37 @@ pgstat_init_relation(Relation rel)
void
pgstat_assoc_relation(Relation rel)
{
+ RelFileLocator locator;
+
Assert(rel->pgstat_enabled);
Assert(rel->pgstat_info == NULL);
+ /*
+ * Don't associate stats for relations without storage and non partitioned
+ * tables.
+ */
+ if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind) &&
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ return;
+
+ if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ locator = rel->rd_locator;
+ else
+ {
+ /*
+ * Partitioned tables don't have storage, so construct a synthetic
+ * locator for statistics tracking. Use a reserved pseudo tablespace
+ * OID that cannot conflict with real tablespaces, and the relation
+ * OID as relNumber. This ensures no collision with regular relations
+ * even after OID wraparound.
+ */
+ locator.dbOid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId);
+ locator.spcOid = PSEUDO_PARTITION_TABLE_SPCOID;
+ locator.relNumber = rel->rd_id;
+ }
+
/* Else find or make the PgStat_TableStatus entry, and update link */
- rel->pgstat_info = pgstat_prep_relation_pending(RelationGetRelid(rel),
- rel->rd_rel->relisshared);
+ rel->pgstat_info = pgstat_prep_relation_pending(locator);
/* don't allow link a stats to multiple relcache entries */
Assert(rel->pgstat_info->relation == NULL);
@@ -167,9 +197,13 @@ pgstat_unlink_relation(Relation rel)
void
pgstat_create_relation(Relation rel)
{
+ /* don't track stats for relations without storage */
+ if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
+ return;
+
pgstat_create_transactional(PGSTAT_KIND_RELATION,
- rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId,
- RelationGetRelid(rel));
+ rel->rd_locator.dbOid,
+ RelFileLocatorToPgStatObjid(rel->rd_locator));
}
/*
@@ -181,9 +215,13 @@ pgstat_drop_relation(Relation rel)
int nest_level = GetCurrentTransactionNestLevel();
PgStat_TableStatus *pgstat_info;
+ /* 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_rel->relisshared ? InvalidOid : MyDatabaseId,
- RelationGetRelid(rel));
+ rel->rd_locator.dbOid,
+ RelFileLocatorToPgStatObjid(rel->rd_locator));
if (!pgstat_should_count_relation(rel))
return;
@@ -213,20 +251,23 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
PgStat_EntryRef *entry_ref;
PgStatShared_Relation *shtabentry;
PgStat_StatTabEntry *tabentry;
- Oid dboid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId);
TimestampTz ts;
PgStat_Counter elapsedtime;
+ RelFileLocator locator;
if (!pgstat_track_counts)
return;
+ locator = rel->rd_locator;
+
/* Store the data in the table's hash table entry. */
ts = GetCurrentTimestamp();
elapsedtime = TimestampDifferenceMilliseconds(starttime, ts);
/* block acquiring lock for the same reason as pgstat_report_autovac() */
- entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, dboid,
- RelationGetRelid(rel), false);
+ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, locator.dbOid,
+ RelFileLocatorToPgStatObjid(locator),
+ false);
shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
tabentry = &shtabentry->stats;
@@ -285,9 +326,9 @@ pgstat_report_analyze(Relation rel,
PgStat_EntryRef *entry_ref;
PgStatShared_Relation *shtabentry;
PgStat_StatTabEntry *tabentry;
- Oid dboid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId);
TimestampTz ts;
PgStat_Counter elapsedtime;
+ RelFileLocator locator;
if (!pgstat_track_counts)
return;
@@ -325,9 +366,25 @@ pgstat_report_analyze(Relation rel,
ts = GetCurrentTimestamp();
elapsedtime = TimestampDifferenceMilliseconds(starttime, ts);
+ if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ locator = rel->rd_locator;
+ else
+ {
+ /*
+ * Partitioned tables don't have storage, so construct a synthetic
+ * locator for statistics tracking. Use a reserved pseudo tablespace
+ * OID that cannot conflict with real tablespaces, and the relation
+ * OID as relNumber. This ensures no collision with regular relations
+ * even after OID wraparound.
+ */
+ locator.dbOid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId);
+ locator.spcOid = PSEUDO_PARTITION_TABLE_SPCOID;
+ locator.relNumber = rel->rd_id;
+ }
/* block acquiring lock for the same reason as pgstat_report_autovac() */
- entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, dboid,
- RelationGetRelid(rel),
+ entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+ locator.dbOid,
+ RelFileLocatorToPgStatObjid(locator),
false);
/* can't get dropped while accessed */
Assert(entry_ref != NULL && entry_ref->shared_stats != NULL);
@@ -468,7 +525,16 @@ pgstat_update_heap_dead_tuples(Relation rel, int delta)
PgStat_StatTabEntry *
pgstat_fetch_stat_tabentry(Oid relid)
{
- return pgstat_fetch_stat_tabentry_ext(IsSharedRelation(relid), relid);
+ return pgstat_fetch_stat_tabentry_ext(relid);
+}
+
+PgStat_StatTabEntry *
+pgstat_fetch_stat_tabentry_by_locator(RelFileLocator locator)
+{
+ return (PgStat_StatTabEntry *) pgstat_fetch_entry(
+ PGSTAT_KIND_RELATION,
+ locator.dbOid,
+ RelFileLocatorToPgStatObjid(locator));
}
/*
@@ -476,12 +542,14 @@ pgstat_fetch_stat_tabentry(Oid relid)
* whether the to-be-accessed table is a shared relation or not.
*/
PgStat_StatTabEntry *
-pgstat_fetch_stat_tabentry_ext(bool shared, Oid reloid)
+pgstat_fetch_stat_tabentry_ext(Oid reloid)
{
- Oid dboid = (shared ? InvalidOid : MyDatabaseId);
+ RelFileLocator locator;
- return (PgStat_StatTabEntry *)
- pgstat_fetch_entry(PGSTAT_KIND_RELATION, dboid, reloid);
+ if (!pgstat_reloid_to_relfilelocator(reloid, &locator))
+ return NULL;
+
+ return pgstat_fetch_stat_tabentry_by_locator(locator);
}
/*
@@ -503,14 +571,17 @@ find_tabstat_entry(Oid rel_id)
PgStat_TableXactStatus *trans;
PgStat_TableStatus *tabentry = NULL;
PgStat_TableStatus *tablestatus = NULL;
+ RelFileLocator locator;
+
+ if (!pgstat_reloid_to_relfilelocator(rel_id, &locator))
+ return NULL;
+
+ entry_ref = pgstat_fetch_pending_entry(PGSTAT_KIND_RELATION,
+ locator.dbOid,
+ RelFileLocatorToPgStatObjid(locator));
- entry_ref = pgstat_fetch_pending_entry(PGSTAT_KIND_RELATION, MyDatabaseId, rel_id);
if (!entry_ref)
- {
- entry_ref = pgstat_fetch_pending_entry(PGSTAT_KIND_RELATION, InvalidOid, rel_id);
- if (!entry_ref)
- return tablestatus;
- }
+ return tablestatus;
tabentry = (PgStat_TableStatus *) entry_ref->pending;
tablestatus = palloc_object(PgStat_TableStatus);
@@ -706,8 +777,12 @@ AtPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state)
record.inserted_pre_truncdrop = trans->inserted_pre_truncdrop;
record.updated_pre_truncdrop = trans->updated_pre_truncdrop;
record.deleted_pre_truncdrop = trans->deleted_pre_truncdrop;
- record.id = tabstat->id;
- record.shared = tabstat->shared;
+
+ if (tabstat->relation != NULL)
+ record.locator = tabstat->relation->rd_locator;
+ else
+ record.locator = tabstat->locator;
+
record.truncdropped = trans->truncdropped;
RegisterTwoPhaseRecord(TWOPHASE_RM_PGSTAT_ID, 0,
@@ -750,7 +825,7 @@ pgstat_twophase_postcommit(FullTransactionId fxid, uint16 info,
PgStat_TableStatus *pgstat_info;
/* Find or create a tabstat entry for the rel */
- pgstat_info = pgstat_prep_relation_pending(rec->id, rec->shared);
+ pgstat_info = pgstat_prep_relation_pending(rec->locator);
/* Same math as in AtEOXact_PgStat, commit case */
pgstat_info->counts.tuples_inserted += rec->tuples_inserted;
@@ -785,8 +860,8 @@ pgstat_twophase_postabort(FullTransactionId fxid, uint16 info,
TwoPhasePgStatRecord *rec = (TwoPhasePgStatRecord *) recdata;
PgStat_TableStatus *pgstat_info;
- /* Find or create a tabstat entry for the rel */
- pgstat_info = pgstat_prep_relation_pending(rec->id, rec->shared);
+ /* Find or create a tabstat entry for the target locator */
+ pgstat_info = pgstat_prep_relation_pending(rec->locator);
/* Same math as in AtEOXact_PgStat, abort case */
if (rec->truncdropped)
@@ -920,17 +995,21 @@ pgstat_relation_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts)
* initialized if not exists.
*/
static PgStat_TableStatus *
-pgstat_prep_relation_pending(Oid rel_id, bool isshared)
+pgstat_prep_relation_pending(RelFileLocator locator)
{
PgStat_EntryRef *entry_ref;
PgStat_TableStatus *pending;
+ uint64 objid;
+
+ objid = RelFileLocatorToPgStatObjid(locator);
entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_RELATION,
- isshared ? InvalidOid : MyDatabaseId,
- rel_id, NULL);
+ locator.dbOid,
+ objid, NULL);
+
pending = entry_ref->pending;
- pending->id = rel_id;
- pending->shared = isshared;
+ pending->id = objid;
+ pending->locator = locator;
return pending;
}
@@ -1009,3 +1088,82 @@ restore_truncdrop_counters(PgStat_TableXactStatus *trans)
trans->tuples_deleted = trans->deleted_pre_truncdrop;
}
}
+
+/*
+ * Convert a relation OID to its corresponding RelFileLocator for statistics
+ * tracking purposes.
+ *
+ * Returns true on success, false if the relation doesn't need statistics
+ * tracking.
+ *
+ * For partitioned tables, constructs a synthetic locator using the relation
+ * OID as relNumber, since they don't have storage.
+ */
+bool
+pgstat_reloid_to_relfilelocator(Oid reloid, RelFileLocator *locator)
+{
+ HeapTuple tuple;
+ Form_pg_class relform;
+ bool result = true;
+
+ /* get the relation's tuple from pg_class */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(reloid));
+
+ if (!HeapTupleIsValid(tuple))
+ return false;
+
+ relform = (Form_pg_class) GETSTRUCT(tuple);
+
+ /* skip relations without storage and non partitioned tables */
+ if (!RELKIND_HAS_STORAGE(relform->relkind) &&
+ relform->relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ ReleaseSysCache(tuple);
+ return false;
+ }
+
+ if (relform->relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ /* build the RelFileLocator */
+ locator->relNumber = relform->relfilenode;
+ locator->spcOid = relform->reltablespace;
+
+ /* handle default tablespace */
+ if (!OidIsValid(locator->spcOid))
+ locator->spcOid = MyDatabaseTableSpace;
+
+ /* handle dbOid for global vs local relations */
+ if (locator->spcOid == GLOBALTABLESPACE_OID)
+ locator->dbOid = InvalidOid;
+ else
+ locator->dbOid = MyDatabaseId;
+
+ /* handle mapped relations */
+ if (!RelFileNumberIsValid(locator->relNumber))
+ {
+ locator->relNumber = RelationMapOidToFilenumber(reloid,
+ relform->relisshared);
+ if (!RelFileNumberIsValid(locator->relNumber))
+ {
+ ReleaseSysCache(tuple);
+ return false;
+ }
+ }
+ }
+ else
+ {
+ /*
+ * Partitioned tables don't have storage, so construct a synthetic
+ * locator for statistics tracking. Use a reserved pseudo tablespace
+ * OID that cannot conflict with real tablespaces, and the relation
+ * OID as relNumber. This ensures no collision with regular relations
+ * even after OID wraparound.
+ */
+ locator->dbOid = (relform->relisshared ? InvalidOid : MyDatabaseId);
+ locator->spcOid = PSEUDO_PARTITION_TABLE_SPCOID;
+ locator->relNumber = relform->oid;
+ }
+
+ ReleaseSysCache(tuple);
+ return result;
+}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index b1df96e7b0b..a5a00d6f018 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -23,13 +23,13 @@
#include "common/ip.h"
#include "funcapi.h"
#include "miscadmin.h"
-#include "pgstat.h"
#include "postmaster/bgworker.h"
#include "replication/logicallauncher.h"
#include "storage/proc.h"
#include "storage/procarray.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/pgstat_internal.h"
#include "utils/timestamp.h"
#define UINT32_ACCESS_ONCE(var) ((uint32)(*((volatile uint32 *)&(var))))
@@ -1961,9 +1961,14 @@ Datum
pg_stat_reset_single_table_counters(PG_FUNCTION_ARGS)
{
Oid taboid = PG_GETARG_OID(0);
- Oid dboid = (IsSharedRelation(taboid) ? InvalidOid : MyDatabaseId);
+ RelFileLocator locator;
- pgstat_reset(PGSTAT_KIND_RELATION, dboid, taboid);
+ /* Get the stats locator from the relation OID */
+ if (!pgstat_reloid_to_relfilelocator(taboid, &locator))
+ PG_RETURN_VOID();
+
+ pgstat_reset(PGSTAT_KIND_RELATION, locator.dbOid,
+ RelFileLocatorToPgStatObjid(locator));
PG_RETURN_VOID();
}
@@ -2317,5 +2322,16 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
uint64 objid = PG_GETARG_INT64(2);
PgStat_Kind kind = pgstat_get_kind_from_str(stats_type);
+ /* Convert relation OID to relfilenode objid */
+ if (kind == PGSTAT_KIND_RELATION)
+ {
+ RelFileLocator locator;
+
+ if (!pgstat_reloid_to_relfilelocator(objid, &locator))
+ PG_RETURN_BOOL(false);
+
+ objid = RelFileLocatorToPgStatObjid(locator);
+ }
+
PG_RETURN_BOOL(pgstat_have_entry(kind, dboid, objid));
}
diff --git a/src/include/catalog/pg_tablespace.dat b/src/include/catalog/pg_tablespace.dat
index c4cde415219..73ed046be31 100644
--- a/src/include/catalog/pg_tablespace.dat
+++ b/src/include/catalog/pg_tablespace.dat
@@ -10,6 +10,10 @@
#
#----------------------------------------------------------------------
+/*
+ * When adding a new one, ensure it does not conflict with
+ * PSEUDO_PARTITION_TABLE_SPCOID.
+ */
[
{ oid => '1663', oid_symbol => 'DEFAULTTABLESPACE_OID',
diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h
index fe7a5ab538f..f511adc3965 100644
--- a/src/include/catalog/pg_tablespace.h
+++ b/src/include/catalog/pg_tablespace.h
@@ -21,6 +21,14 @@
#include "catalog/genbki.h"
#include "catalog/pg_tablespace_d.h" /* IWYU pragma: export */
+/*
+ * Reserved tablespace OID for partitioned table pseudo locators.
+ * This is not an actual tablespace, just a reserved value to distinguish
+ * partitioned table statistics from regular table statistics. Ensures it does
+ * not conflict with the ones in pg_tablespace.dat.
+ */
+#define PSEUDO_PARTITION_TABLE_SPCOID 1665
+
/* ----------------
* pg_tablespace definition. cpp turns this into
* typedef struct FormData_pg_tablespace
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 9bb777c3d5a..e9e4d32c3b8 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -16,6 +16,7 @@
#include "portability/instr_time.h"
#include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */
#include "replication/conflict.h"
+#include "storage/relfilelocator.h"
#include "utils/backend_progress.h" /* for backward compatibility */ /* IWYU pragma: export */
#include "utils/backend_status.h" /* for backward compatibility */ /* IWYU pragma: export */
#include "utils/pgstat_kind.h"
@@ -34,6 +35,12 @@
/* Default directory to store temporary statistics data in */
#define PG_STAT_TMP_DIR "pg_stat_tmp"
+/*
+ * Build a pgstat key Objid based on a RelFileLocator.
+ */
+#define RelFileLocatorToPgStatObjid(locator) \
+ (((uint64) (locator).spcOid << 32) | (locator).relNumber)
+
/* Values for track_functions GUC variable --- order is significant! */
typedef enum TrackFunctionsLevel
{
@@ -174,11 +181,11 @@ typedef struct PgStat_TableCounts
*/
typedef struct PgStat_TableStatus
{
- Oid id; /* table's OID */
- bool shared; /* is it a shared catalog? */
+ uint64 id; /* hash of relfilelocator for stats key */
struct PgStat_TableXactStatus *trans; /* lowest subxact's counts */
PgStat_TableCounts counts; /* event counts to be sent */
Relation relation; /* rel that is using this entry */
+ RelFileLocator locator; /* table's relfilelocator */
} PgStat_TableStatus;
/* ----------
@@ -734,8 +741,8 @@ extern void pgstat_twophase_postabort(FullTransactionId fxid, uint16 info,
void *recdata, uint32 len);
extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry(Oid relid);
-extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry_ext(bool shared,
- Oid reloid);
+extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry_by_locator(RelFileLocator locator);
+extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry_ext(Oid reloid);
extern PgStat_TableStatus *find_tabstat_entry(Oid rel_id);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9b8fbae00ed..42f29496551 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -765,6 +765,7 @@ extern void PostPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state);
extern bool pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);
extern void pgstat_relation_delete_pending_cb(PgStat_EntryRef *entry_ref);
extern void pgstat_relation_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts);
+extern bool pgstat_reloid_to_relfilelocator(Oid reloid, RelFileLocator *locator);
/*
diff --git a/src/test/recovery/t/029_stats_restart.pl b/src/test/recovery/t/029_stats_restart.pl
index cdc427dbc78..4d00087dc6f 100644
--- a/src/test/recovery/t/029_stats_restart.pl
+++ b/src/test/recovery/t/029_stats_restart.pl
@@ -55,10 +55,10 @@ trigger_funcrel_stat();
# verify stats objects exist
$sect = "initial";
-is(have_stats('database', $dboid, 0), 't', "$sect: db stats do exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 't', "$sect: db stats do exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
't', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
't', "$sect: relation stats do exist");
# regular shutdown
@@ -79,10 +79,10 @@ copy($og_stats, $statsfile) or die "Copy failed: $!";
$node->start;
$sect = "copy";
-is(have_stats('database', $dboid, 0), 't', "$sect: db stats do exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 't', "$sect: db stats do exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
't', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
't', "$sect: relation stats do exist");
$node->stop('immediate');
@@ -96,10 +96,10 @@ $node->start;
# stats should have been discarded
$sect = "post immediate";
-is(have_stats('database', $dboid, 0), 'f', "$sect: db stats do not exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 'f', "$sect: db stats do not exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
'f', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
'f', "$sect: relation stats do not exist");
# get rid of backup statsfile
@@ -110,10 +110,10 @@ unlink $statsfile or die "cannot unlink $statsfile $!";
trigger_funcrel_stat();
$sect = "post immediate, new";
-is(have_stats('database', $dboid, 0), 't', "$sect: db stats do exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 't', "$sect: db stats do exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
't', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
't', "$sect: relation stats do exist");
# regular shutdown
@@ -129,10 +129,10 @@ $node->start;
# no stats present due to invalid stats file
$sect = "invalid_overwrite";
-is(have_stats('database', $dboid, 0), 'f', "$sect: db stats do not exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 'f', "$sect: db stats do not exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
'f', "$sect: function stats do not exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
'f', "$sect: relation stats do not exist");
@@ -145,10 +145,10 @@ append_file($og_stats, "XYZ");
$node->start;
$sect = "invalid_append";
-is(have_stats('database', $dboid, 0), 'f', "$sect: db stats do not exist");
-is(have_stats('function', $dboid, $funcoid),
+is(have_stats($connect_db, 'database', $dboid, 0), 'f', "$sect: db stats do not exist");
+is(have_stats($db_under_test, 'function', $dboid, $funcoid),
'f', "$sect: function stats do not exist");
-is(have_stats('relation', $dboid, $tableoid),
+is(have_stats($db_under_test, 'relation', $dboid, $tableoid),
'f', "$sect: relation stats do not exist");
@@ -307,9 +307,9 @@ sub trigger_funcrel_stat
sub have_stats
{
- my ($kind, $dboid, $objid) = @_;
+ my ($db, $kind, $dboid, $objid) = @_;
- return $node->safe_psql($connect_db,
+ return $node->safe_psql($db,
"SELECT pg_stat_have_stats('$kind', $dboid, $objid)");
}
--
2.34.1
--zLA8DXafBEP889wv
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v11-0002-handle-relation-statistics-correctly-during-rewr.patch"
^ permalink raw reply [nested|flat] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ 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; 160+ 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] 160+ messages in thread
end of thread, other threads:[~2026-07-15 08:37 UTC | newest]
Thread overview: 160+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2025-10-01 09:45 [PATCH v11 1/2] Key PGSTAT_KIND_RELATION by relfile locator 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-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]>
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