Back-patch "Refactor code in tablecmds.c to check and process tablespace moves"

Back-patch commits 4c9c359d38ff1e2de388eedd860785be6a49201c and
24843297a96d7be16cc3f4b090aacfc6e5e6839e to v13 and v12.  Before those
commits, we held the modifiable copy of the relation's pg_class row
throughout a table_relation_copy_data().  That can last long enough to
copy MaxBlockNumber of data.  A subsequent fix will hold LockTuple() for
the lifespan of that modifiable copy.  By back-patching this first, we
avoid a needless long-duration LOCKTAG_TUPLE.

Discussion: https://postgr.es/m/20231027214946.79.nmisch@google.com
This commit is contained in:
Noah Misch 2024-09-24 15:25:24 -07:00
parent 8ec99d0b79
commit dc845383cd
2 changed files with 123 additions and 91 deletions

View File

@ -3027,6 +3027,112 @@ SetRelationHasSubclass(Oid relationId, bool relhassubclass)
table_close(relationRelation, RowExclusiveLock);
}
/*
* CheckRelationTableSpaceMove
* Check if relation can be moved to new tablespace.
*
* NOTE: The caller must hold AccessExclusiveLock on the relation.
*
* Returns true if the relation can be moved to the new tablespace; raises
* an error if it is not possible to do the move; returns false if the move
* would have no effect.
*/
bool
CheckRelationTableSpaceMove(Relation rel, Oid newTableSpaceId)
{
Oid oldTableSpaceId;
/*
* No work if no change in tablespace. Note that MyDatabaseTableSpace is
* stored as 0.
*/
oldTableSpaceId = rel->rd_rel->reltablespace;
if (newTableSpaceId == oldTableSpaceId ||
(newTableSpaceId == MyDatabaseTableSpace && oldTableSpaceId == 0))
return false;
/*
* We cannot support moving mapped relations into different tablespaces.
* (In particular this eliminates all shared catalogs.)
*/
if (RelationIsMapped(rel))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot move system relation \"%s\"",
RelationGetRelationName(rel))));
/* Cannot move a non-shared relation into pg_global */
if (newTableSpaceId == GLOBALTABLESPACE_OID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("only shared relations can be placed in pg_global tablespace")));
/*
* Do not allow moving temp tables of other backends ... their local
* buffer manager is not going to cope.
*/
if (RELATION_IS_OTHER_TEMP(rel))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot move temporary tables of other sessions")));
return true;
}
/*
* SetRelationTableSpace
* Set new reltablespace and relfilenode in pg_class entry.
*
* newTableSpaceId is the new tablespace for the relation, and
* newRelFileNode its new filenode. If newRelFileNode is InvalidOid,
* this field is not updated.
*
* NOTE: The caller must hold AccessExclusiveLock on the relation.
*
* The caller of this routine had better check if a relation can be
* moved to this new tablespace by calling CheckRelationTableSpaceMove()
* first, and is responsible for making the change visible with
* CommandCounterIncrement().
*/
void
SetRelationTableSpace(Relation rel,
Oid newTableSpaceId,
Oid newRelFileNode)
{
Relation pg_class;
HeapTuple tuple;
Form_pg_class rd_rel;
Oid reloid = RelationGetRelid(rel);
Assert(CheckRelationTableSpaceMove(rel, newTableSpaceId));
/* Get a modifiable copy of the relation's pg_class row. */
pg_class = table_open(RelationRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", reloid);
rd_rel = (Form_pg_class) GETSTRUCT(tuple);
/* Update the pg_class row. */
rd_rel->reltablespace = (newTableSpaceId == MyDatabaseTableSpace) ?
InvalidOid : newTableSpaceId;
if (OidIsValid(newRelFileNode))
rd_rel->relfilenode = newRelFileNode;
CatalogTupleUpdate(pg_class, &tuple->t_self, tuple);
/*
* Record dependency on tablespace. This is only required for relations
* that have no physical storage.
*/
if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind))
changeDependencyOnTablespace(RelationRelationId, reloid,
rd_rel->reltablespace);
heap_freetuple(tuple);
table_close(pg_class, RowExclusiveLock);
}
/*
* renameatt_check - basic sanity checks before attribute rename
*/
@ -13086,13 +13192,9 @@ static void
ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode)
{
Relation rel;
Oid oldTableSpace;
Oid reltoastrelid;
Oid newrelfilenode;
RelFileNode newrnode;
Relation pg_class;
HeapTuple tuple;
Form_pg_class rd_rel;
List *reltoastidxids = NIL;
ListCell *lc;
@ -13101,45 +13203,15 @@ ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode)
*/
rel = relation_open(tableOid, lockmode);
/*
* No work if no change in tablespace.
*/
oldTableSpace = rel->rd_rel->reltablespace;
if (newTableSpace == oldTableSpace ||
(newTableSpace == MyDatabaseTableSpace && oldTableSpace == 0))
/* Check first if relation can be moved to new tablespace */
if (!CheckRelationTableSpaceMove(rel, newTableSpace))
{
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), 0);
relation_close(rel, NoLock);
return;
}
/*
* We cannot support moving mapped relations into different tablespaces.
* (In particular this eliminates all shared catalogs.)
*/
if (RelationIsMapped(rel))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot move system relation \"%s\"",
RelationGetRelationName(rel))));
/* Can't move a non-shared relation into pg_global */
if (newTableSpace == GLOBALTABLESPACE_OID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("only shared relations can be placed in pg_global tablespace")));
/*
* Don't allow moving temp tables of other backends ... their local buffer
* manager is not going to cope.
*/
if (RELATION_IS_OTHER_TEMP(rel))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot move temporary tables of other sessions")));
reltoastrelid = rel->rd_rel->reltoastrelid;
/* Fetch the list of indexes on toast relation if necessary */
if (OidIsValid(reltoastrelid))
@ -13150,14 +13222,6 @@ ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode)
relation_close(toastRel, lockmode);
}
/* Get a modifiable copy of the relation's pg_class row */
pg_class = table_open(RelationRelationId, RowExclusiveLock);
tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(tableOid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", tableOid);
rd_rel = (Form_pg_class) GETSTRUCT(tuple);
/*
* Relfilenodes are not unique in databases across tablespaces, so we need
* to allocate a new one in the new tablespace.
@ -13188,18 +13252,13 @@ ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode)
*
* NB: This wouldn't work if ATExecSetTableSpace() were allowed to be
* executed on pg_class or its indexes (the above copy wouldn't contain
* the updated pg_class entry), but that's forbidden above.
* the updated pg_class entry), but that's forbidden with
* CheckRelationTableSpaceMove().
*/
rd_rel->reltablespace = (newTableSpace == MyDatabaseTableSpace) ? InvalidOid : newTableSpace;
rd_rel->relfilenode = newrelfilenode;
CatalogTupleUpdate(pg_class, &tuple->t_self, tuple);
SetRelationTableSpace(rel, newTableSpace, newrelfilenode);
InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0);
heap_freetuple(tuple);
table_close(pg_class, RowExclusiveLock);
relation_close(rel, NoLock);
/* Make sure the reltablespace change is visible */
@ -13225,56 +13284,25 @@ ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode)
static void
ATExecSetTableSpaceNoStorage(Relation rel, Oid newTableSpace)
{
HeapTuple tuple;
Oid oldTableSpace;
Relation pg_class;
Form_pg_class rd_rel;
Oid reloid = RelationGetRelid(rel);
/*
* Shouldn't be called on relations having storage; these are processed in
* phase 3.
*/
Assert(!RELKIND_HAS_STORAGE(rel->rd_rel->relkind));
/* Can't allow a non-shared relation in pg_global */
if (newTableSpace == GLOBALTABLESPACE_OID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("only shared relations can be placed in pg_global tablespace")));
/*
* No work if no change in tablespace.
*/
oldTableSpace = rel->rd_rel->reltablespace;
if (newTableSpace == oldTableSpace ||
(newTableSpace == MyDatabaseTableSpace && oldTableSpace == 0))
/* check if relation can be moved to its new tablespace */
if (!CheckRelationTableSpaceMove(rel, newTableSpace))
{
InvokeObjectPostAlterHook(RelationRelationId, reloid, 0);
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel),
0);
return;
}
/* Get a modifiable copy of the relation's pg_class row */
pg_class = table_open(RelationRelationId, RowExclusiveLock);
/* Update can be done, so change reltablespace */
SetRelationTableSpace(rel, newTableSpace, InvalidOid);
tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for relation %u", reloid);
rd_rel = (Form_pg_class) GETSTRUCT(tuple);
/* update the pg_class row */
rd_rel->reltablespace = (newTableSpace == MyDatabaseTableSpace) ? InvalidOid : newTableSpace;
CatalogTupleUpdate(pg_class, &tuple->t_self, tuple);
/* Record dependency on tablespace */
changeDependencyOnTablespace(RelationRelationId,
reloid, rd_rel->reltablespace);
InvokeObjectPostAlterHook(RelationRelationId, reloid, 0);
heap_freetuple(tuple);
table_close(pg_class, RowExclusiveLock);
InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), 0);
/* Make sure the reltablespace change is visible */
CommandCounterIncrement();

View File

@ -58,6 +58,10 @@ extern void ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_
extern void SetRelationHasSubclass(Oid relationId, bool relhassubclass);
extern bool CheckRelationTableSpaceMove(Relation rel, Oid newTableSpaceId);
extern void SetRelationTableSpace(Relation rel, Oid newTableSpaceId,
Oid newRelFileNode);
extern ObjectAddress renameatt(RenameStmt *stmt);
extern ObjectAddress renameatt_type(RenameStmt *stmt);