Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1w8kFQ-000ptm-0T for pgsql-hackers@arkaria.postgresql.org; Fri, 03 Apr 2026 19:32:05 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1w8kFN-00Df10-2n for pgsql-hackers@arkaria.postgresql.org; Fri, 03 Apr 2026 19:32:02 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1w8kFM-00Df0X-2Z for pgsql-hackers@lists.postgresql.org; Fri, 03 Apr 2026 19:32:01 +0000 Received: from fout-a2-smtp.messagingengine.com ([103.168.172.145]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1w8kFE-00000000PFp-2eUS for pgsql-hackers@lists.postgresql.org; Fri, 03 Apr 2026 19:32:00 +0000 Received: from phl-compute-06.internal (phl-compute-06.internal [10.202.2.46]) by mailfout.phl.internal (Postfix) with ESMTP id 5A2CDEC01B0; Fri, 3 Apr 2026 15:31:51 -0400 (EDT) Received: from phl-frontend-03 ([10.202.2.162]) by phl-compute-06.internal (MEProxy); Fri, 03 Apr 2026 15:31:51 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-transfer-encoding :content-type:content-type:date:date:feedback-id:feedback-id :from:from:in-reply-to:in-reply-to:message-id:mime-version :reply-to:subject:subject:to:to:x-me-proxy:x-me-sender :x-me-sender:x-sasl-enc; s=fm2; t=1775244711; x=1775331111; bh=G 7EAF6yxfQF4Vh0qZ6HnyU23E4q2iWCC/sZZzZXQg3M=; b=Ab5b6ChZTAqaGrkzo YjSjyALFTuN25YgUfyCS6ObHBVhGZzDQnsZotqsLobY1Em//R5lNqyyr8ggXTtuB u5eMTnNbByftzbIA1gZMYi6MlcLKaW0eTP8DngwxNgc3sRhuTlK/kLeWg78Z3yx3 hTjPfGOIB+iTbsyHsabvvjkhOXa76QcppS8opFnR4wQCIDvnQ27BT/BzLAITA/xA RK+o4J/DPwo7AKsVaTJXAKBM2lzapUbRN5/h4WES6Le5RQd14vEpfWfjrquZ+y6X mjjUBlN6V12rK3elZYQhcc5mj3uZWD7ie0IBWv+BzHeRICHqbTZJcFRtdjhFqpAQ Sp/Og== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeefhedrtddtgdeljeelucetufdoteggodetrfdotf fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceurghi lhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmnecujfgurh epfffhvfevuffkgggtugfgjgesmhekreertddtjeenucfhrhhomheptehlvhgrrhhoucfj vghrrhgvrhgruceorghlvhhhvghrrhgvsegrlhhvhhdrnhhoqdhiphdrohhrgheqnecugg ftrfgrthhtvghrnhepudelkeekgfdttdettdekgfduvdeiffelteegjeeihfetheekveef tedtuddvhfeunecuffhomhgrihhnpegvnhhtvghrphhrihhsvggusgdrtghomhenucevlh hushhtvghrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpegrlhhvhhgvrhhr vgesrghlvhhhrdhnohdqihhprdhorhhgpdhnsggprhgtphhtthhopeejpdhmohguvgepsh hmthhpohhuthdprhgtphhtthhopegrhhestgihsggvrhhtvggtrdgrthdprhgtphhtthho pegrmhhithdrkhgrphhilhgrudeisehgmhgrihhlrdgtohhmpdhrtghpthhtohepsghovg hkvgifuhhrmhdophhoshhtghhrvghssehgmhgrihhlrdgtohhmpdhrtghpthhtohepmhhi hhgrihhlnhhikhgrlhgrhigvuhesghhmrghilhdrtghomhdprhgtphhtthhopehsrhhinh grthhhvddufeefsehgmhgrihhlrdgtohhmpdhrtghpthhtohepphhgshhqlhdqhhgrtghk vghrsheslhhishhtshdrphhoshhtghhrvghsqhhlrdhorhhgpdhrtghpthhtoheprhhosg esgiiiihhllhgrrdhnvght X-ME-Proxy: Feedback-ID: ia2694551:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA; Fri, 3 Apr 2026 15:31:50 -0400 (EDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=alvh.no-ip.org; s=schmee; t=1775244708; bh=8Si0cZYVf72z87Zbe9wnx3bxZ0mGUmvXKhcNjSzOQoM=; h=Date:From:To:Cc:Subject:In-Reply-To:From; b=nQQbH/gNaglCP/Ifl0AnVJ+tWMWHTJCWcCn5wStKg31k1S6AWAdT75mhwWnb6WJux e+0zhoywoIBKi1zkIpxBdlchZECGHQOVFIiMOqiVpFWPFGVBrCVLDLUkzxIqBeMSFy omK+un6/ipAcyfPbzGmSD7+8dnUITxNz6YYqBDEKqQgyH4FthZmcFdwk08GpSEru8B mG5LaqlqSko87QSE6I26NOPCpmkWQZbhuJzpiOUFP+h2nDRDNUJfBPYhFAziYVbW+i MuywoGYAFizz0xjT+issmVdNUkheubnIT81LazvYWI0oBYoNeuuY80O07Do13Ry+Dk sEO+nKTLFp41g== Received: by schmee.kurilemu.internal (Postfix, from userid 1000) id 19B387C; Fri, 03 Apr 2026 21:31:48 +0200 (CEST) Date: Fri, 3 Apr 2026 21:31:48 +0200 From: Alvaro Herrera To: Antonin Houska Cc: Srinath Reddy Sadipiralla , Amit Kapila , Mihail Nikalayeu , Matthias van de Meent , Pg Hackers , Robert Treat Subject: Re: Adding REPACK [concurrently] Message-ID: <202604031922.t6lgu5a2alwo@alvherre.pgsql> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="r2slln3zpmilwu22" Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: <202604031448.3nakw63kxkmr@alvherre.pgsql> List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --r2slln3zpmilwu22 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit On 2026-Apr-03, Alvaro Herrera wrote: > - I polished the patch to reserve replication slots for REPACK. Given > the new implementation of 0006 that was submitted implies that we can > now run multiple repacks concurrently, I changed the default of 1 to 5. Srinath let me know that this new part was causing CI failures on Windows. This version v51 should be okay (or, at least, it passes for me on CI). Additional changes worth mentioning: - I think it's nicer for the index_create() API to get a bit to indicate suppression of progress reporting; so existing callers don't need to do anything. I guess this is mostly a matter of taste. - I incorporated Srinath's fix for the PreventInTransactionBlock block. - When CheckSlotRequirements() is to complain about "max_replication_slots or max_repack_replication_slots", it seems actually nicer to say exactly which one of these is the cause of the problem. This is easy to change; patch 0010 does it; it requires passing down a "repack" flag all the way from CheckLogicalDecodingRequirements() and it needs to add an argument to CreateInitDecodingContext(), which is perhaps not so great. On the whole I'm inclined to do it anyway, but I'm about equally happy to leave it alone. This is what would change: original: ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("replication slots can only be used if \"%s\" > 0 or \"%s\" > 0", - "max_replication_slots", "max_repack_replication_slots"))); patched: ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("replication slots can only be used if \"%s\" > 0", + repack ? "max_repack_replication_slots" : "max_replication_slots")); -- Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/ "This is a foot just waiting to be shot" (Andrew Dunstan) --r2slln3zpmilwu22 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v51-0001-Make-index_concurrently_create_copy-more-general.patch" From 5972c694e9cc7d891b4b69dedfe4ddc946a6069a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Tue, 24 Mar 2026 19:02:58 +0100 Subject: [PATCH v51 01/10] Make index_concurrently_create_copy more general Add a 'boolean concurrent' option, and make it work for both cases. Also rename it to index_create_copy. This allows it to be reused for other purposes -- specifically, for REPACK CONCURRENTLY. With the CONCURRENTLY option, REPACK cannot simply swap the heap file and rebuild its indexes. Instead, it needs to build a separate set of indexes (including system catalog entries) *before* the actual swap, to reduce the time AccessExclusiveLock needs to be held for. This approach is different from what CREATE INDEX CONCURRENTLY does. Per a suggestion from Mihail Nikalayeu. Author: Antonin Houska Discussion: https://postgr.es/m/41104.1754922120@localhost --- src/backend/catalog/index.c | 41 ++++++++++++++++++++++---------- src/backend/commands/indexcmds.c | 9 +++---- src/include/catalog/index.h | 7 +++--- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 1ccfa687f05..e418d67e8e4 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -1289,17 +1289,17 @@ index_create(Relation heapRelation, } /* - * index_concurrently_create_copy + * index_create_copy * - * Create concurrently an index based on the definition of the one provided by - * caller. The index is inserted into catalogs and needs to be built later - * on. This is called during concurrent reindex processing. + * Create an index based on the definition of the one provided by caller. The + * index is inserted into catalogs. If 'concurrently' is TRUE, it needs to be + * built later on; otherwise it's built immediately. * * "tablespaceOid" is the tablespace to use for this index. */ Oid -index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, - Oid tablespaceOid, const char *newName) +index_create_copy(Relation heapRelation, bool concurrently, + Oid oldIndexId, Oid tablespaceOid, const char *newName) { Relation indexRelation; IndexInfo *oldInfo, @@ -1318,6 +1318,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, List *indexColNames = NIL; List *indexExprs = NIL; List *indexPreds = NIL; + int flags = 0; indexRelation = index_open(oldIndexId, RowExclusiveLock); @@ -1328,7 +1329,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, * Concurrent build of an index with exclusion constraints is not * supported. */ - if (oldInfo->ii_ExclusionOps != NULL) + if (oldInfo->ii_ExclusionOps != NULL && concurrently) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("concurrent index creation for exclusion constraints is not supported"))); @@ -1384,9 +1385,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, } /* - * Build the index information for the new index. Note that rebuild of - * indexes with exclusion constraints is not supported, hence there is no - * need to fill all the ii_Exclusion* fields. + * Build the index information for the new index. */ newInfo = makeIndexInfo(oldInfo->ii_NumIndexAttrs, oldInfo->ii_NumIndexKeyAttrs, @@ -1395,11 +1394,24 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, indexPreds, oldInfo->ii_Unique, oldInfo->ii_NullsNotDistinct, - false, /* not ready for inserts */ - true, + !concurrently, /* isready */ + concurrently, /* concurrent */ indexRelation->rd_indam->amsummarizing, oldInfo->ii_WithoutOverlaps); + /* fetch exclusion constraint info if any */ + if (indexRelation->rd_index->indisexclusion) + { + /* + * XXX Beware: we're making newInfo point to oldInfo-owned memory. It + * would be more orthodox to palloc+memcpy, but we don't need that + * here at present. + */ + newInfo->ii_ExclusionOps = oldInfo->ii_ExclusionOps; + newInfo->ii_ExclusionProcs = oldInfo->ii_ExclusionProcs; + newInfo->ii_ExclusionStrats = oldInfo->ii_ExclusionStrats; + } + /* * Extract the list of column names and the column numbers for the new * index information. All this information will be used for the index @@ -1436,6 +1448,9 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, stattargets[i].isnull = isnull; } + if (concurrently) + flags = INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT; + /* * Now create the new index. * @@ -1459,7 +1474,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, indcoloptions->values, stattargets, reloptionsDatum, - INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT, + flags, 0, true, /* allow table to be a system catalog? */ false, /* is_internal? */ diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 373e8234794..cba379810c7 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -3989,10 +3989,11 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein tablespaceid = indexRel->rd_rel->reltablespace; /* Create new index definition based on given index */ - newIndexId = index_concurrently_create_copy(heapRel, - idx->indexId, - tablespaceid, - concurrentName); + newIndexId = index_create_copy(heapRel, + true, + idx->indexId, + tablespaceid, + concurrentName); /* * Now open the relation of the new index, a session-level lock is diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h index a38e95bc0eb..ed9e4c37d27 100644 --- a/src/include/catalog/index.h +++ b/src/include/catalog/index.h @@ -101,10 +101,9 @@ extern Oid index_create(Relation heapRelation, #define INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS (1 << 4) #define INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS (1 << 5) -extern Oid index_concurrently_create_copy(Relation heapRelation, - Oid oldIndexId, - Oid tablespaceOid, - const char *newName); +extern Oid index_create_copy(Relation heapRelation, bool concurrently, + Oid oldIndexId, Oid tablespaceOid, + const char *newName); extern void index_concurrently_build(Oid heapRelationId, Oid indexRelationId); -- 2.47.3 --r2slln3zpmilwu22 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v51-0002-Rename-cluster.c-h-repack.c-h.patch" From 720095da25eabe98d9fbf4dd8c714c1d1db95e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Tue, 31 Mar 2026 18:55:29 +0200 Subject: [PATCH v51 02/10] Rename cluster.c/h -> repack.c/h --- src/backend/commands/Makefile | 2 +- src/backend/commands/matview.c | 2 +- src/backend/commands/meson.build | 2 +- src/backend/commands/{cluster.c => repack.c} | 6 +++--- src/backend/commands/tablecmds.c | 2 +- src/backend/commands/vacuum.c | 6 +++--- src/backend/storage/ipc/procsignal.c | 1 + src/backend/tcop/postgres.c | 1 + src/backend/tcop/utility.c | 2 +- src/include/commands/{cluster.h => repack.h} | 12 ++++++------ 10 files changed, 19 insertions(+), 17 deletions(-) rename src/backend/commands/{cluster.c => repack.c} (99%) rename src/include/commands/{cluster.h => repack.h} (90%) diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index c10fdba2bbb..fe1bba3a9b9 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -18,7 +18,6 @@ OBJS = \ amcmds.o \ analyze.o \ async.o \ - cluster.o \ collationcmds.o \ comment.o \ constraint.o \ @@ -51,6 +50,7 @@ OBJS = \ proclang.o \ propgraphcmds.o \ publicationcmds.o \ + repack.o \ schemacmds.o \ seclabel.o \ sequence.o \ diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index d3be8939011..5db4fe75dce 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -24,8 +24,8 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_opclass.h" -#include "commands/cluster.h" #include "commands/matview.h" +#include "commands/repack.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" #include "executor/executor.h" diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index 90c7e37a429..f624aae74af 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -6,7 +6,6 @@ backend_sources += files( 'amcmds.c', 'analyze.c', 'async.c', - 'cluster.c', 'collationcmds.c', 'comment.c', 'constraint.c', @@ -39,6 +38,7 @@ backend_sources += files( 'proclang.c', 'propgraphcmds.c', 'publicationcmds.c', + 'repack.c', 'schemacmds.c', 'seclabel.c', 'sequence.c', diff --git a/src/backend/commands/cluster.c b/src/backend/commands/repack.c similarity index 99% rename from src/backend/commands/cluster.c rename to src/backend/commands/repack.c index f241e18b153..20f0a572236 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/repack.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * cluster.c + * repack.c * REPACK a table; formerly known as CLUSTER. VACUUM FULL also uses * parts of this code. * @@ -10,7 +10,7 @@ * * * IDENTIFICATION - * src/backend/commands/cluster.c + * src/backend/commands/repack.c * *------------------------------------------------------------------------- */ @@ -33,9 +33,9 @@ #include "catalog/pg_am.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" -#include "commands/cluster.h" #include "commands/defrem.h" #include "commands/progress.h" +#include "commands/repack.h" #include "commands/tablecmds.h" #include "commands/vacuum.h" #include "miscadmin.h" diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 0ce2e81f9c2..e2882a50b3b 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -57,10 +57,10 @@ #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" -#include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/repack.h" #include "commands/sequence.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index 0ed363d1c85..b179b62b5c8 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -9,7 +9,7 @@ * * VACUUM for heap AM is implemented in vacuumlazy.c, parallel vacuum in * vacuumparallel.c, ANALYZE in analyze.c, and VACUUM FULL is a variant of - * CLUSTER, handled in cluster.c. + * REPACK, handled in repack.c. * * * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group @@ -38,9 +38,9 @@ #include "catalog/pg_database.h" #include "catalog/pg_inherits.h" #include "commands/async.h" -#include "commands/cluster.h" #include "commands/defrem.h" #include "commands/progress.h" +#include "commands/repack.h" #include "commands/vacuum.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -2293,7 +2293,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params, if ((params.options & VACOPT_VERBOSE) != 0) cluster_params.options |= CLUOPT_VERBOSE; - /* VACUUM FULL is a variant of REPACK; see cluster.c */ + /* VACUUM FULL is a variant of REPACK; see repack.c */ cluster_rel(REPACK_COMMAND_VACUUMFULL, rel, InvalidOid, &cluster_params); /* cluster_rel closes the relation, but keeps lock */ diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 7e017c8d53b..7cef6e43661 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -19,6 +19,7 @@ #include "access/parallel.h" #include "commands/async.h" +#include "commands/repack.h" #include "miscadmin.h" #include "pgstat.h" #include "port/pg_bitutils.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 10be60011ad..9fbaa5c00f0 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -39,6 +39,7 @@ #include "commands/event_trigger.h" #include "commands/explain_state.h" #include "commands/prepare.h" +#include "commands/repack.h" #include "common/pg_prng.h" #include "jit/jit.h" #include "libpq/libpq.h" diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 2b609bfc824..5f8c766c4be 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -26,7 +26,6 @@ #include "catalog/toasting.h" #include "commands/alter.h" #include "commands/async.h" -#include "commands/cluster.h" #include "commands/collationcmds.h" #include "commands/comment.h" #include "commands/conversioncmds.h" @@ -46,6 +45,7 @@ #include "commands/proclang.h" #include "commands/propgraphcmds.h" #include "commands/publicationcmds.h" +#include "commands/repack.h" #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" diff --git a/src/include/commands/cluster.h b/src/include/commands/repack.h similarity index 90% rename from src/include/commands/cluster.h rename to src/include/commands/repack.h index d6b62c747e8..85061158b0c 100644 --- a/src/include/commands/cluster.h +++ b/src/include/commands/repack.h @@ -1,17 +1,17 @@ /*------------------------------------------------------------------------- * - * cluster.h - * header file for postgres cluster command stuff + * repack.h + * header file for the REPACK command * * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * - * src/include/commands/cluster.h + * src/include/commands/repack.h * *------------------------------------------------------------------------- */ -#ifndef CLUSTER_H -#define CLUSTER_H +#ifndef REPACK_H +#define REPACK_H #include "nodes/parsenodes.h" #include "parser/parse_node.h" @@ -52,4 +52,4 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, MultiXactId cutoffMulti, char newrelpersistence); -#endif /* CLUSTER_H */ +#endif /* REPACK_H */ -- 2.47.3 --r2slln3zpmilwu22 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v51-0003-Add-CONCURRENTLY-option-to-REPACK-command.patch" From 5fb1d1c7f58ddea23fea49b0637cb595a2d2bceb Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Wed, 11 Mar 2026 15:16:45 +0100 Subject: [PATCH v51 03/10] Add CONCURRENTLY option to REPACK command. The REPACK command copies the relation data into a new file, creates new indexes and eventually swaps the files. To make sure that the old file does not change during the copying, the relation is locked in an exclusive mode, which prevents applications from both reading and writing. (To keep the data consistent, we'd only need to prevent the applications from writing, but even reading needs to be blocked before we can swap the files - otherwise some applications could continue using the old file. Currently, REPACK takes the simple approach and acquires the exclusive lock in the beginning. This patch introduces an alternative workflow, which only requires the exclusive lock when the relation (and index) files are being swapped. (Supposedly, the swapping should be pretty fast.) On the other hand, when we copy the data to the new file, we allow applications to read from the relation and even to write to it. First, we scan the relation using a "historic snapshot", and insert all the tuples satisfying this snapshot into the new relation. Second, logical decoding is used to capture the data changes done by applications during the copying (i.e. changes not yet committed from the perspective of the historic snapshot mentioned above), and those are applied to the new file before we acquire the exclusive lock that we need to swap the files. (Of course, more data changes can take place while we are waiting for the lock - these will be applied to the new file after we have acquired the lock and before we swap the files.) While the "concurrent data" changes are applied at specific stages (we cannot do that until the intial copy is finished and indexes are built), a background worker performs the decoding all the time. This way we minimize the amount of not-yet-decoded WAL, so that archiving / recycling of WAL segments is not delayed much. The decoded changes are written to files and passed to the backed performing REPACK. Since the logical decoding system, during its startup, waits until all the transactions which already have XID assigned have finished, there is a risk of deadlock if a transaction that already changed anything in the database tries to acquire a conflicting lock on the table REPACK CONCURRENTLY is working on. As an example, consider transaction running CREATE INDEX command on the table that is being REPACKed CONCURRENTLY. On the other hand, DML commands (INSERT, UPDATE, DELETE) are not a problem as their lock does not conflict with REPACK CONCURRENTLY. The current approach is that we accept the risk. If we tried to avoid it, it'd be necessary to unlock the table before the logical decoding is setup and lock it again afterwards. Such temporary unlocking would imply re-checking if the table still meets all the requirements for REPACK CONCURRENTLY. The WAL records produced by running DML commands on the new relation are intentionally not fed to the logical decoding system. Doing so would introduce significant overhead, and - as the new relation is never available for logical replication - it would be useless. --- doc/src/sgml/monitoring.sgml | 37 +- doc/src/sgml/mvcc.sgml | 12 +- doc/src/sgml/ref/repack.sgml | 118 +- src/Makefile | 1 + src/backend/access/heap/heapam.c | 25 +- src/backend/access/heap/heapam_handler.c | 269 ++- src/backend/access/heap/rewriteheap.c | 6 +- src/backend/catalog/system_views.sql | 19 +- src/backend/commands/Makefile | 1 + src/backend/commands/matview.c | 1 + src/backend/commands/meson.build | 1 + src/backend/commands/repack.c | 1843 ++++++++++++++++- src/backend/commands/repack_worker.c | 549 +++++ src/backend/commands/tablecmds.c | 1 + src/backend/commands/vacuum.c | 12 +- src/backend/executor/nodeModifyTable.c | 2 +- src/backend/libpq/pqmq.c | 5 + src/backend/meson.build | 1 + src/backend/postmaster/bgworker.c | 6 +- src/backend/replication/logical/decode.c | 28 +- src/backend/replication/logical/logical.c | 6 +- .../replication/pgoutput_repack/Makefile | 32 + .../replication/pgoutput_repack/meson.build | 18 + .../pgoutput_repack/pgoutput_repack.c | 290 +++ src/backend/storage/ipc/procsignal.c | 3 + .../storage/lmgr/generate-lwlocknames.pl | 2 +- src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 1 + src/bin/psql/tab-complete.in.c | 4 +- src/include/access/heapam_xlog.h | 2 + src/include/access/tableam.h | 16 +- src/include/commands/progress.h | 17 +- src/include/commands/repack.h | 15 +- src/include/commands/repack_internal.h | 128 ++ src/include/replication/decode.h | 4 + src/include/storage/lockdefs.h | 4 +- src/include/storage/procsignal.h | 1 + src/test/modules/injection_points/Makefile | 2 + .../injection_points/expected/repack.out | 113 + .../expected/repack_toast.out | 65 + src/test/modules/injection_points/meson.build | 2 + .../injection_points/specs/repack.spec | 142 ++ .../injection_points/specs/repack_toast.spec | 112 + src/test/regress/expected/rules.out | 19 +- src/tools/pgindent/typedefs.list | 6 + 45 files changed, 3674 insertions(+), 270 deletions(-) create mode 100644 src/backend/commands/repack_worker.c create mode 100644 src/backend/replication/pgoutput_repack/Makefile create mode 100644 src/backend/replication/pgoutput_repack/meson.build create mode 100644 src/backend/replication/pgoutput_repack/pgoutput_repack.c create mode 100644 src/include/commands/repack_internal.h create mode 100644 src/test/modules/injection_points/expected/repack.out create mode 100644 src/test/modules/injection_points/expected/repack_toast.out create mode 100644 src/test/modules/injection_points/specs/repack.spec create mode 100644 src/test/modules/injection_points/specs/repack_toast.spec diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index bb75ed1069b..a0a155eb10b 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -6985,14 +6985,35 @@ FROM pg_stat_get_backend_idset() AS backendid; - heap_tuples_written bigint + heap_tuples_inserted bigint - Number of heap tuples written. + Number of heap tuples inserted. This counter only advances when the phase is seq scanning heap, - index scanning heap - or writing new heap. + index scanning heap, + writing new heap + or catch-up. + + + + + + heap_tuples_updated bigint + + + Number of heap tuples updated. + This counter only advances when the phase is catch-up. + + + + + + heap_tuples_deleted bigint + + + Number of heap tuples deleted. + This counter only advances when the phase is catch-up. @@ -7073,6 +7094,14 @@ FROM pg_stat_get_backend_idset() AS backendid; REPACK is currently writing the new heap. + + catch-up + + REPACK CONCURRENTLY is currently processing the DML + commands that other transactions executed during any of the preceding + phases. + + swapping relation files diff --git a/doc/src/sgml/mvcc.sgml b/doc/src/sgml/mvcc.sgml index e775260936a..241caeb3593 100644 --- a/doc/src/sgml/mvcc.sgml +++ b/doc/src/sgml/mvcc.sgml @@ -1845,15 +1845,17 @@ SELECT pg_advisory_lock(q.id) FROM Caveats - Some DDL commands, currently only TRUNCATE and the - table-rewriting forms of ALTER TABLE, are not + Some commands, currently only TRUNCATE, the + table-rewriting forms of ALTER + TABLE and REPACK with + the CONCURRENTLY option, are not MVCC-safe. This means that after the truncation or rewrite commits, the table will appear empty to concurrent transactions, if they are using a - snapshot taken before the DDL command committed. This will only be an + snapshot taken before the command committed. This will only be an issue for a transaction that did not access the table in question - before the DDL command started — any transaction that has done so + before the command started — any transaction that has done so would hold at least an ACCESS SHARE table lock, - which would block the DDL command until that transaction completes. + which would block the truncating or rewriting command until that transaction completes. So these commands will not cause any apparent inconsistency in the table contents for successive queries on the target table, but they could cause visible inconsistency between the contents of the target diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml index 8ccf7c7a417..bec18c44cc8 100644 --- a/doc/src/sgml/ref/repack.sgml +++ b/doc/src/sgml/ref/repack.sgml @@ -28,6 +28,7 @@ REPACK [ ( option [, ...] ) ] USING VERBOSE [ boolean ] ANALYZE [ boolean ] + CONCURRENTLY [ boolean ] and table_and_columns is: @@ -54,7 +55,8 @@ REPACK [ ( option [, ...] ) ] USING processes every table and materialized view in the current database that the current user has the MAINTAIN privilege on. This form of REPACK cannot be executed inside a transaction - block. + block. Also, this form is not allowed if + the CONCURRENTLY option is used. @@ -67,7 +69,8 @@ REPACK [ ( option [, ...] ) ] USING When a table is being repacked, an ACCESS EXCLUSIVE lock is acquired on it. This prevents any other database operations (both reads and writes) from operating on the table until the REPACK - is finished. + is finished. If you want to keep the table accessible during the repacking, + consider using the CONCURRENTLY option. @@ -198,6 +201,117 @@ REPACK [ ( option [, ...] ) ] USING + + CONCURRENTLY + + + Allow other transactions to use the table while it is being repacked. + + + + Internally, REPACK copies the contents of the table + (ignoring dead tuples) into a new file, sorted by the specified index, + and also creates a new file for each index. Then it swaps the old and + new files for the table and all the indexes, and deletes the old + files. The ACCESS EXCLUSIVE lock is needed to make + sure that the old files do not change during the processing because the + changes would get lost due to the swap. + + + + With the CONCURRENTLY option, the ACCESS + EXCLUSIVE lock is only acquired to swap the table and index + files. The data changes that took place during the creation of the new + table and index files are captured using logical decoding + () and applied before + the ACCESS EXCLUSIVE lock is requested. Thus the lock + is typically held only for the time needed to swap the files, which + should be pretty short. However, the time might still be noticeable if + too many data changes have been done to the table while + REPACK was waiting for the lock: those changes must + be processed just before the files are swapped, while the + ACCESS EXCLUSIVE lock is being held. + + + + Note that REPACK with the + CONCURRENTLY option does not try to order the rows + inserted into the table after the repacking started. Also + note REPACK might fail to complete due to DDL + commands executed on the table by other transactions during the + repacking. + + + + + In addition to the temporary space requirements explained in + , + the CONCURRENTLY option can add to the usage of + temporary space a bit more. The reason is that other transactions can + perform DML operations which cannot be applied to the new file until + REPACK has copied all the existing tuples from the + old file. Thus the tuples inserted into the old file during the copying + are also stored separately in a temporary file, until they can be + processed. + + + + + The CONCURRENTLY option cannot be used in the + following cases: + + + + + The table is UNLOGGED. + + + + + + The table is partitioned. + + + + + + The table lacks a primary key and index-based replica identity. + + + + + + The table is a system catalog or a TOAST table. + + + + + + REPACK is executed inside a transaction block. + + + + + + The max_replication_slots + configuration parameter does not allow for creation of an additional + replication slot. + + + + + + + + REPACK with the CONCURRENTLY + option is not MVCC-safe, see for + details. + + + + + + VERBOSE diff --git a/src/Makefile b/src/Makefile index 2f31a2f20a7..b18c9a14ffa 100644 --- a/src/Makefile +++ b/src/Makefile @@ -23,6 +23,7 @@ SUBDIRS = \ interfaces \ backend/replication/libpqwalreceiver \ backend/replication/pgoutput \ + backend/replication/pgoutput_repack \ fe_utils \ bin \ pl \ diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index 6bff0032db2..fb1f5b7280e 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -61,7 +61,8 @@ static HeapTuple heap_prepare_insert(Relation relation, HeapTuple tup, static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf, Buffer newbuf, HeapTuple oldtup, HeapTuple newtup, HeapTuple old_key_tuple, - bool all_visible_cleared, bool new_all_visible_cleared); + bool all_visible_cleared, bool new_all_visible_cleared, + bool walLogical); #ifdef USE_ASSERT_CHECKING static void check_lock_if_inplace_updateable_rel(Relation relation, const ItemPointerData *otid, @@ -2877,6 +2878,7 @@ heap_delete(Relation relation, const ItemPointerData *tid, uint16 new_infomask, new_infomask2; bool changingPart = (options & TABLE_DELETE_CHANGING_PARTITION) != 0; + bool walLogical = (options & TABLE_DELETE_NO_LOGICAL) == 0; bool have_tuple_lock = false; bool iscombo; bool all_visible_cleared = false; @@ -3111,7 +3113,8 @@ l1: * Compute replica identity tuple before entering the critical section so * we don't PANIC upon a memory allocation failure. */ - old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, &old_key_copied); + old_key_tuple = walLogical ? + ExtractReplicaIdentity(relation, &tp, true, &old_key_copied) : NULL; /* * If this is the first possibly-multixact-able operation in the current @@ -3201,6 +3204,15 @@ l1: xlrec.flags |= XLH_DELETE_CONTAINS_OLD_KEY; } + /* + * Unlike UPDATE, DELETE is decoded even if there is no old key, so it + * does not help to clear both XLH_DELETE_CONTAINS_OLD_TUPLE and + * XLH_DELETE_CONTAINS_OLD_KEY. Thus we need an extra flag. TODO + * Consider not decoding tuples w/o the old tuple/key instead. + */ + if (!walLogical) + xlrec.flags |= XLH_DELETE_NO_LOGICAL; + XLogBeginInsert(); XLogRegisterData(&xlrec, SizeOfHeapDelete); @@ -3351,6 +3363,7 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup, HeapTuple heaptup; HeapTuple old_key_tuple = NULL; bool old_key_copied = false; + bool walLogical = (options & TABLE_UPDATE_NO_LOGICAL) == 0; Page page, newpage; BlockNumber block; @@ -4232,7 +4245,8 @@ l2: newbuf, &oldtup, heaptup, old_key_tuple, all_visible_cleared, - all_visible_cleared_new); + all_visible_cleared_new, + walLogical); if (newbuf != buffer) { PageSetLSN(newpage, recptr); @@ -8908,7 +8922,8 @@ static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf, Buffer newbuf, HeapTuple oldtup, HeapTuple newtup, HeapTuple old_key_tuple, - bool all_visible_cleared, bool new_all_visible_cleared) + bool all_visible_cleared, bool new_all_visible_cleared, + bool walLogical) { xl_heap_update xlrec; xl_heap_header xlhdr; @@ -8919,7 +8934,7 @@ log_heap_update(Relation reln, Buffer oldbuf, suffixlen = 0; XLogRecPtr recptr; Page page = BufferGetPage(newbuf); - bool need_tuple_data = RelationIsLogicallyLogged(reln); + bool need_tuple_data = walLogical && RelationIsLogicallyLogged(reln); bool init; int bufflags; diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 1be8ea4845a..9082d09aa3d 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -51,6 +51,11 @@ static void reform_and_rewrite_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, Datum *values, bool *isnull, RewriteState rwstate); +static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, + Relation NewHeap, Datum *values, bool *isnull, + BulkInsertState bistate); +static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, + Relation NewHeap, Datum *values, bool *isnull); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -700,13 +705,15 @@ static void heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, Relation OldIndex, bool use_sort, TransactionId OldestXmin, + Snapshot snapshot, TransactionId *xid_cutoff, MultiXactId *multi_cutoff, double *num_tuples, double *tups_vacuumed, double *tups_recently_dead) { - RewriteState rwstate; + RewriteState rwstate = NULL; + BulkInsertState bistate = NULL; IndexScanDesc indexScan; TableScanDesc tableScan; HeapScanDesc heapScan; @@ -720,6 +727,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool *isnull; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; + bool concurrent = snapshot != NULL; /* Remember if it's a system catalog */ is_system_catalog = IsSystemRelation(OldHeap); @@ -735,9 +743,14 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, values = palloc_array(Datum, natts); isnull = palloc_array(bool, natts); - /* Initialize the rewrite operation */ - rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin, *xid_cutoff, - *multi_cutoff); + /* + * Initialize the rewrite operation. + */ + if (!concurrent) + rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin, + *xid_cutoff, *multi_cutoff); + else + bistate = GetBulkInsertState(); /* Set up sorting if wanted */ @@ -752,6 +765,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, * Prepare to scan the OldHeap. To ensure we see recently-dead tuples * that still need to be copied, we scan with SnapshotAny and use * HeapTupleSatisfiesVacuum for the visibility test. + * + * In the CONCURRENTLY case, we do regular MVCC visibility tests, using + * the snapshot passed by the caller. */ if (OldIndex != NULL && !use_sort) { @@ -768,7 +784,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, tableScan = NULL; heapScan = NULL; - indexScan = index_beginscan(OldHeap, OldIndex, SnapshotAny, NULL, 0, 0, + indexScan = index_beginscan(OldHeap, OldIndex, + snapshot ? snapshot : SnapshotAny, + NULL, 0, 0, SO_NONE); index_rescan(indexScan, NULL, 0, NULL, 0); } @@ -778,7 +796,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP); - tableScan = table_beginscan(OldHeap, SnapshotAny, 0, (ScanKey) NULL, + tableScan = table_beginscan(OldHeap, + snapshot ? snapshot : SnapshotAny, + 0, (ScanKey) NULL, SO_NONE); heapScan = (HeapScanDesc) tableScan; indexScan = NULL; @@ -855,83 +875,91 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, buf = hslot->buffer; /* - * To be able to guarantee that we can set the hint bit, acquire an - * exclusive lock on the old buffer. We need the hint bits, set in - * heapam_relation_copy_for_cluster() -> HeapTupleSatisfiesVacuum(), - * to be set, as otherwise reform_and_rewrite_tuple() -> - * rewrite_heap_tuple() will get confused. Specifically, - * rewrite_heap_tuple() checks for HEAP_XMAX_INVALID in the old tuple - * to determine whether to check the old-to-new mapping hash table. - * - * It'd be better if we somehow could avoid setting hint bits on the - * old page. One reason to use VACUUM FULL are very bloated tables - - * rewriting most of the old table during VACUUM FULL doesn't exactly - * help... + * Regarding CONCURRENTLY, see the comments on MVCC snapshot above. */ - LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); - - switch (HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf)) + if (!concurrent) { - case HEAPTUPLE_DEAD: - /* Definitely dead */ - isdead = true; - break; - case HEAPTUPLE_RECENTLY_DEAD: - *tups_recently_dead += 1; - pg_fallthrough; - case HEAPTUPLE_LIVE: - /* Live or recently dead, must copy it */ - isdead = false; - break; - case HEAPTUPLE_INSERT_IN_PROGRESS: + /* + * To be able to guarantee that we can set the hint bit, acquire + * an exclusive lock on the old buffer. We need the hint bits, set + * in heapam_relation_copy_for_cluster() -> + * HeapTupleSatisfiesVacuum(), to be set, as otherwise + * reform_and_rewrite_tuple() -> rewrite_heap_tuple() will get + * confused. Specifically, rewrite_heap_tuple() checks for + * HEAP_XMAX_INVALID in the old tuple to determine whether to + * check the old-to-new mapping hash table. + * + * It'd be better if we somehow could avoid setting hint bits on + * the old page. One reason to use VACUUM FULL are very bloated + * tables - rewriting most of the old table during VACUUM FULL + * doesn't exactly help... + */ + LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); - /* - * Since we hold exclusive lock on the relation, normally the - * only way to see this is if it was inserted earlier in our - * own transaction. However, it can happen in system - * catalogs, since we tend to release write lock before commit - * there. Give a warning if neither case applies; but in any - * case we had better copy it. - */ - if (!is_system_catalog && - !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data))) - elog(WARNING, "concurrent insert in progress within table \"%s\"", - RelationGetRelationName(OldHeap)); - /* treat as live */ - isdead = false; - break; - case HEAPTUPLE_DELETE_IN_PROGRESS: - - /* - * Similar situation to INSERT_IN_PROGRESS case. - */ - if (!is_system_catalog && - !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple->t_data))) - elog(WARNING, "concurrent delete in progress within table \"%s\"", - RelationGetRelationName(OldHeap)); - /* treat as recently dead */ - *tups_recently_dead += 1; - isdead = false; - break; - default: - elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); - isdead = false; /* keep compiler quiet */ - break; - } - - LockBuffer(buf, BUFFER_LOCK_UNLOCK); - - if (isdead) - { - *tups_vacuumed += 1; - /* heap rewrite module still needs to see it... */ - if (rewrite_heap_dead_tuple(rwstate, tuple)) + switch (HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf)) { - /* A previous recently-dead tuple is now known dead */ - *tups_vacuumed += 1; - *tups_recently_dead -= 1; + case HEAPTUPLE_DEAD: + /* Definitely dead */ + isdead = true; + break; + case HEAPTUPLE_RECENTLY_DEAD: + *tups_recently_dead += 1; + pg_fallthrough; + case HEAPTUPLE_LIVE: + /* Live or recently dead, must copy it */ + isdead = false; + break; + case HEAPTUPLE_INSERT_IN_PROGRESS: + + /* + * As long as we hold exclusive lock on the relation, + * normally the only way to see this is if it was inserted + * earlier in our own transaction. However, it can happen + * in system catalogs, since we tend to release write lock + * before commit there. Give a warning if neither case + * applies; but in any case we had better copy it. + */ + if (!is_system_catalog && + !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data))) + elog(WARNING, "concurrent insert in progress within table \"%s\"", + RelationGetRelationName(OldHeap)); + /* treat as live */ + isdead = false; + break; + case HEAPTUPLE_DELETE_IN_PROGRESS: + + /* + * Similar situation to INSERT_IN_PROGRESS case. + */ + if (!is_system_catalog && + !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple->t_data))) + elog(WARNING, "concurrent delete in progress within table \"%s\"", + RelationGetRelationName(OldHeap)); + /* treat as recently dead */ + *tups_recently_dead += 1; + isdead = false; + break; + default: + elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); + isdead = false; /* keep compiler quiet */ + break; + } + + LockBuffer(buf, BUFFER_LOCK_UNLOCK); + + if (isdead) + { + *tups_vacuumed += 1; + /* heap rewrite module still needs to see it... */ + if (rewrite_heap_dead_tuple(rwstate, tuple)) + { + /* A previous recently-dead tuple is now known dead */ + *tups_vacuumed += 1; + *tups_recently_dead -= 1; + } + + continue; } - continue; } *num_tuples += 1; @@ -950,12 +978,16 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, { const int ct_index[] = { PROGRESS_REPACK_HEAP_TUPLES_SCANNED, - PROGRESS_REPACK_HEAP_TUPLES_WRITTEN + PROGRESS_REPACK_HEAP_TUPLES_INSERTED }; int64 ct_val[2]; - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + if (!concurrent) + reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, + values, isnull, rwstate); + else + heap_insert_for_repack(tuple, OldHeap, NewHeap, + values, isnull, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -1003,12 +1035,17 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, break; n_tuples += 1; - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + if (!concurrent) + reform_and_rewrite_tuple(tuple, + OldHeap, NewHeap, + values, isnull, + rwstate); + else + heap_insert_for_repack(tuple, OldHeap, NewHeap, + values, isnull, bistate); + /* Report n_tuples */ - pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_WRITTEN, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } @@ -1016,7 +1053,10 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, } /* Write out any remaining tuples, and fsync if needed */ - end_heap_rewrite(rwstate); + if (rwstate) + end_heap_rewrite(rwstate); + if (bistate) + FreeBulkInsertState(bistate); /* Clean up */ pfree(values); @@ -2414,27 +2454,66 @@ static void reform_and_rewrite_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, Datum *values, bool *isnull, RewriteState rwstate) +{ + HeapTuple copiedTuple; + + copiedTuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + + /* The heap rewrite module does the rest */ + rewrite_heap_tuple(rwstate, tuple, copiedTuple); + + heap_freetuple(copiedTuple); +} + +/* + * Insert tuple when processing REPACK CONCURRENTLY. + * + * rewriteheap.c is not used in the CONCURRENTLY case because it'd be + * difficult to do the same in the catch-up phase (as the logical + * decoding does not provide us with sufficient visibility + * information). Thus we must use heap_insert() both during the + * catch-up and here. + * + * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical + * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops + * this relation, so no logical replication subscription should need the data. + * + * BulkInsertState is used because many tuples are inserted in the typical + * case. + */ +static void +heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, + Datum *values, bool *isnull, BulkInsertState bistate) +{ + tuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + + heap_insert(NewHeap, tuple, GetCurrentCommandId(true), + HEAP_INSERT_NO_LOGICAL, bistate); + + heap_freetuple(tuple); +} + +/* + * Deform tuple, set values of dropped columns to NULL, form a new tuple and + * return it. + */ +static HeapTuple +reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, + Datum *values, bool *isnull) { TupleDesc oldTupDesc = RelationGetDescr(OldHeap); TupleDesc newTupDesc = RelationGetDescr(NewHeap); - HeapTuple copiedTuple; int i; heap_deform_tuple(tuple, oldTupDesc, values, isnull); - /* Be sure to null out any dropped columns */ for (i = 0; i < newTupDesc->natts; i++) { if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) isnull[i] = true; } - copiedTuple = heap_form_tuple(newTupDesc, values, isnull); - - /* The heap rewrite module does the rest */ - rewrite_heap_tuple(rwstate, tuple, copiedTuple); - - heap_freetuple(copiedTuple); + return heap_form_tuple(newTupDesc, values, isnull); } /* diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c index f707b102c72..5a5398a76ae 100644 --- a/src/backend/access/heap/rewriteheap.c +++ b/src/backend/access/heap/rewriteheap.c @@ -621,9 +621,9 @@ raw_heap_insert(RewriteState state, HeapTuple tup) uint32 options = HEAP_INSERT_SKIP_FSM; /* - * While rewriting the heap for VACUUM FULL / CLUSTER, make sure data - * for the TOAST table are not logically decoded. The main heap is - * WAL-logged as XLOG FPI records, which are not logically decoded. + * While rewriting the heap for REPACK, make sure data for the TOAST + * table are not logically decoded. The main heap is WAL-logged as + * XLOG FPI records, which are not logically decoded. */ options |= HEAP_INSERT_NO_LOGICAL; diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index e54018004db..75f97cb23e3 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1343,16 +1343,19 @@ CREATE VIEW pg_stat_progress_repack AS WHEN 2 THEN 'index scanning heap' WHEN 3 THEN 'sorting tuples' WHEN 4 THEN 'writing new heap' - WHEN 5 THEN 'swapping relation files' - WHEN 6 THEN 'rebuilding index' - WHEN 7 THEN 'performing final cleanup' + WHEN 5 THEN 'catch-up' + WHEN 6 THEN 'swapping relation files' + WHEN 7 THEN 'rebuilding index' + WHEN 8 THEN 'performing final cleanup' END AS phase, CAST(S.param3 AS oid) AS repack_index_relid, S.param4 AS heap_tuples_scanned, - S.param5 AS heap_tuples_written, - S.param6 AS heap_blks_total, - S.param7 AS heap_blks_scanned, - S.param8 AS index_rebuild_count + S.param5 AS heap_tuples_inserted, + S.param6 AS heap_tuples_updated, + S.param7 AS heap_tuples_deleted, + S.param8 AS heap_blks_total, + S.param9 AS heap_blks_scanned, + S.param10 AS index_rebuild_count FROM pg_stat_get_progress_info('REPACK') AS S LEFT JOIN pg_database D ON S.datid = D.oid; @@ -1370,7 +1373,7 @@ CREATE VIEW pg_stat_progress_cluster AS phase, repack_index_relid AS cluster_index_relid, heap_tuples_scanned, - heap_tuples_written, + heap_tuples_inserted + heap_tuples_updated AS heap_tuples_written, heap_blks_total, heap_blks_scanned, index_rebuild_count diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index fe1bba3a9b9..5b9d084977e 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -51,6 +51,7 @@ OBJS = \ propgraphcmds.o \ publicationcmds.o \ repack.o \ + repack_worker.o \ schemacmds.o \ seclabel.o \ sequence.o \ diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 5db4fe75dce..f7d8007f796 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -893,6 +893,7 @@ static void refresh_by_heap_swap(Oid matviewOid, Oid OIDNewHeap, char relpersistence) { finish_heap_swap(matviewOid, OIDNewHeap, false, false, true, true, + true, /* reindex */ RecentXmin, ReadNextMultiXactId(), relpersistence); } diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index f624aae74af..9f258d566eb 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -39,6 +39,7 @@ backend_sources += files( 'propgraphcmds.c', 'publicationcmds.c', 'repack.c', + 'repack_worker.c', 'schemacmds.c', 'seclabel.c', 'sequence.c', diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 20f0a572236..03829892d57 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -4,6 +4,22 @@ * REPACK a table; formerly known as CLUSTER. VACUUM FULL also uses * parts of this code. * + * There are two somewhat different ways to rewrite a table. In non- + * concurrent mode, it's easy: take AccessExclusiveLock, create a new + * transient relation, copy the tuples over to the relfilenode of the new + * relation, swap the relfilenodes, then drop the old relation. + * + * In concurrent mode, we lock the table with only ShareUpdateExclusiveLock, + * then do an initial copy as above. However, while the tuples are being + * copied, concurrent transactions could modify the table. To cope with those + * changes, we rely on logical decoding to obtain them from WAL. A bgworker + * consumes WAL while the initial copy is ongoing (to prevent excessive WAL + * from being reserved), and accumulates the changes in a file. Once the + * initial copy is complete, we read the changes from the file and re-apply + * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to + * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold + * a strong lock on the table is much reduced, and the bloat is eliminated. + * * * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California @@ -17,6 +33,7 @@ #include "postgres.h" #include "access/amapi.h" +#include "access/detoast.h" #include "access/heapam.h" #include "access/multixact.h" #include "access/relscan.h" @@ -24,6 +41,7 @@ #include "access/toast_internals.h" #include "access/transam.h" #include "access/xact.h" +#include "access/xloginsert.h" #include "catalog/catalog.h" #include "catalog/dependency.h" #include "catalog/heap.h" @@ -31,22 +49,30 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_constraint.h" +#include "catalog/pg_control.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" #include "commands/defrem.h" #include "commands/progress.h" #include "commands/repack.h" +#include "commands/repack_internal.h" #include "commands/tablecmds.h" #include "commands/vacuum.h" +#include "executor/executor.h" +#include "libpq/pqformat.h" +#include "libpq/pqmq.h" #include "miscadmin.h" #include "optimizer/optimizer.h" #include "pgstat.h" #include "storage/bufmgr.h" #include "storage/lmgr.h" #include "storage/predicate.h" +#include "storage/proc.h" #include "utils/acl.h" #include "utils/fmgroids.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" @@ -54,6 +80,7 @@ #include "utils/relmapper.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/wait_event_types.h" /* * This struct is used to pass around the information on tables to be @@ -66,12 +93,79 @@ typedef struct Oid indexOid; } RelToCluster; +/* + * The first file exported by the decoding worker must contain a snapshot, the + * following ones contain the data changes. + */ +#define WORKER_FILE_SNAPSHOT 0 + +/* + * Information needed to apply concurrent data changes. + */ +typedef struct ChangeContext +{ + /* The relation the changes are applied to. */ + Relation cc_rel; + + /* Needed to update indexes of rel_dst. */ + ResultRelInfo *cc_rri; + EState *cc_estate; + + /* + * Existing tuples to UPDATE and DELETE are located via this index. We + * keep the scankey in partially initialized state to avoid repeated work. + * sk_argument is completed on the fly. + */ + Relation cc_ident_index; + ScanKey cc_ident_key; + int cc_ident_key_nentries; + + /* Sequential number of the file containing the changes. */ + int cc_file_seq; +} ChangeContext; + +/* FIXME probably in repack_worker.h */ +pid_t backend_pid; +ProcNumber backend_proc_number; + +/* + * Backend-local information to control the decoding worker. + */ +typedef struct DecodingWorker +{ + /* The worker. */ + BackgroundWorkerHandle *handle; + + /* DecodingWorkerShared is in this segment. */ + dsm_segment *seg; + + /* Handle of the error queue. */ + shm_mq_handle *error_mqh; +} DecodingWorker; + +/* Pointer to currently running decoding worker. */ +static DecodingWorker *decoding_worker = NULL; + +/* + * Is there a message sent by a repack worker that the backend needs to + * receive? + */ +volatile sig_atomic_t RepackMessagePending = false; + +static LOCKMODE RepackLockLevel(bool concurrent); static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, - Oid indexOid, Oid userid, int options); -static void rebuild_relation(Relation OldHeap, Relation index, bool verbose); + Oid indexOid, Oid userid, LOCKMODE lmode, + int options); +static void check_repack_concurrently_requirements(Relation rel, + Oid *ident_idx_p); +static void rebuild_relation(Relation OldHeap, Relation index, bool verbose, + Oid ident_idx); static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, - bool verbose, bool *pSwapToastByContent, - TransactionId *pFreezeXid, MultiXactId *pCutoffMulti); + Snapshot snapshot, + bool verbose, + bool *pSwapToastByContent, + TransactionId *pFreezeXid, + MultiXactId *pCutoffMulti); static List *get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt); static List *get_tables_to_repack_partitioned(RepackCommand cmd, @@ -79,10 +173,47 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd, MemoryContext permcxt); static bool repack_is_permitted_for_relation(RepackCommand cmd, Oid relid, Oid userid); + +static void apply_concurrent_changes(BufFile *file, ChangeContext *chgcxt); +static void apply_concurrent_insert(Relation rel, TupleTableSlot *slot, + ChangeContext *chgcxt); +static void apply_concurrent_update(Relation rel, TupleTableSlot *spilled_tuple, + TupleTableSlot *ondisk_tuple, + ChangeContext *chgcxt); +static void apply_concurrent_delete(Relation rel, TupleTableSlot *slot); +static void restore_tuple(BufFile *file, Relation relation, + TupleTableSlot *slot); +static void adjust_toast_pointers(Relation relation, TupleTableSlot *dest, + TupleTableSlot *src); +static bool find_target_tuple(Relation rel, ChangeContext *chgcxt, + TupleTableSlot *locator, + TupleTableSlot *received); +static void process_concurrent_changes(XLogRecPtr end_of_wal, + ChangeContext *chgcxt, + bool done); +static void initialize_change_context(ChangeContext *chgcxt, + Relation relation, + Oid ident_index_id); +static void release_change_context(ChangeContext *chgcxt); +static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, + Oid identIdx, + TransactionId frozenXid, + MultiXactId cutoffMulti); +static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); +static void copy_index_constraints(Relation old_index, Oid new_index_id, + Oid new_heap_id); static Relation process_single_relation(RepackStmt *stmt, + LOCKMODE lockmode, + bool isTopLevel, ClusterParams *params); static Oid determine_clustered_index(Relation rel, bool usingindex, const char *indexname); + +static void start_repack_decoding_worker(Oid relid); +static void stop_repack_decoding_worker(void); +static Snapshot get_initial_snapshot(DecodingWorker *worker); + +static void ProcessRepackMessage(StringInfo msg); static const char *RepackCommandAsString(RepackCommand cmd); @@ -115,6 +246,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) ClusterParams params = {0}; Relation rel = NULL; MemoryContext repack_context; + LOCKMODE lockmode; List *rtcs; /* Parse option list */ @@ -125,6 +257,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) else if (strcmp(opt->defname, "analyze") == 0 || strcmp(opt->defname, "analyse") == 0) params.options |= defGetBoolean(opt) ? CLUOPT_ANALYZE : 0; + else if (strcmp(opt->defname, "concurrently") == 0 && + defGetBoolean(opt)) + { + if (stmt->command != REPACK_COMMAND_REPACK) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("CONCURRENTLY option not supported for %s", + RepackCommandAsString(stmt->command))); + params.options |= CLUOPT_CONCURRENT; + } else ereport(ERROR, errcode(ERRCODE_SYNTAX_ERROR), @@ -134,13 +276,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) parser_errposition(pstate, opt->location)); } + /* Determine the lock mode to use. */ + lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0); + /* * If a single relation is specified, process it and we're done ... unless * the relation is a partitioned table, in which case we fall through. */ if (stmt->relation != NULL) { - rel = process_single_relation(stmt, ¶ms); + rel = process_single_relation(stmt, lockmode, isTopLevel, ¶ms); if (rel == NULL) return; /* all done */ } @@ -156,10 +301,29 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) "REPACK (ANALYZE)")); /* - * By here, we know we are in a multi-table situation. In order to avoid - * holding locks for too long, we want to process each table in its own - * transaction. This forces us to disallow running inside a user - * transaction block. + * By here, we know we are in a multi-table situation. + * + * Concurrent processing is currently considered rather special (e.g. in + * terms of resources consumed) so it is not performed in bulk. + */ + if (params.options & CLUOPT_CONCURRENT) + { + if (rel != NULL) + { + Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + ereport(ERROR, + errmsg("REPACK CONCURRENTLY not supported for partitioned tables"), + errhint("Consider running the command for individual partitions.")); + } + else + ereport(ERROR, + errmsg("REPACK CONCURRENTLY requires explicit table name")); + } + + /* + * In order to avoid holding locks for too long, we want to process each + * table in its own transaction. This forces us to disallow running + * inside a user transaction block. */ PreventInTransactionBlock(isTopLevel, RepackCommandAsString(stmt->command)); @@ -168,6 +332,12 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) "Repack", ALLOCSET_DEFAULT_SIZES); + /* + * Since we open a new transaction for each relation, we have to check + * that the relation still is what we think it is. + * + * In single-transaction CLUSTER, we don't need the overhead. + */ params.options |= CLUOPT_RECHECK; /* @@ -253,7 +423,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) * Open the target table, coping with the case where it has been * dropped. */ - rel = try_table_open(rtc->tableOid, AccessExclusiveLock); + rel = try_table_open(rtc->tableOid, lockmode); if (rel == NULL) { CommitTransactionCommand(); @@ -264,7 +434,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) PushActiveSnapshot(GetTransactionSnapshot()); /* Process this table */ - cluster_rel(stmt->command, rel, rtc->indexOid, ¶ms); + cluster_rel(stmt->command, rel, rtc->indexOid, ¶ms, isTopLevel); /* cluster_rel closes the relation, but keeps lock */ PopActiveSnapshot(); @@ -278,6 +448,22 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) MemoryContextDelete(repack_context); } +/* + * In the non-concurrent case, we obtain AccessExclusiveLock throughout the + * operation to avoid any lock-upgrade hazards. In the concurrent case, we + * grab ShareUpdateExclusiveLock (jsut like VACUUM) for most of the + * processing and only acquire AccessExclusiveLock at the end, to swap the + * relation -- supposedly for a short time. + */ +static LOCKMODE +RepackLockLevel(bool concurrent) +{ + if (concurrent) + return ShareUpdateExclusiveLock; + else + return AccessExclusiveLock; +} + /* * cluster_rel * @@ -293,22 +479,51 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) * If indexOid is InvalidOid, the table will be rewritten in physical order * instead of index order. * + * Note that, in the concurrent case, the function releases the lock at some + * point, in order to get AccessExclusiveLock for the final steps (i.e. to + * swap the relation files). To make things simpler, the caller should expect + * OldHeap to be closed on return, regardless CLUOPT_CONCURRENT. (The + * AccessExclusiveLock is kept till the end of the transaction.) + * * 'cmd' indicates which command is being executed, to be used for error * messages. */ void cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, - ClusterParams *params) + ClusterParams *params, bool isTopLevel) { Oid tableOid = RelationGetRelid(OldHeap); + Relation index; + LOCKMODE lmode; Oid save_userid; int save_sec_context; int save_nestlevel; bool verbose = ((params->options & CLUOPT_VERBOSE) != 0); bool recheck = ((params->options & CLUOPT_RECHECK) != 0); - Relation index; + bool concurrent = ((params->options & CLUOPT_CONCURRENT) != 0); + Oid ident_idx = InvalidOid; - Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false)); + /* Determine the lock mode to use. */ + lmode = RepackLockLevel(concurrent); + + /* + * Check some preconditions in the concurrent case. This also obtains the + * replica index OID. + */ + if (concurrent) + { + /* + * Make sure we're not in a transaction block. + * + * The reason is that repack_setup_logical_decoding() could deadlock + * if there's an XID already assigned. It would be possible to run in + * a transaction block if we had no XID, but this restriction is + * simpler for users to understand and we don't lose anything. + */ + PreventInTransactionBlock(isTopLevel, "REPACK (CONCURRENTLY)"); + + check_repack_concurrently_requirements(OldHeap, &ident_idx); + } /* Check for user-requested abort. */ CHECK_FOR_INTERRUPTS(); @@ -328,16 +543,15 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, RestrictSearchPath(); /* - * Since we may open a new transaction for each relation, we have to check - * that the relation still is what we think it is. + * Recheck that the relation is still what it was when we started. * - * If this is a single-transaction CLUSTER, we can skip these tests. We - * *must* skip the one on indisclustered since it would reject an attempt - * to cluster a not-previously-clustered index. + * Note that it's critical to skip this in single-relation CLUSTER; + * otherwise, we would reject an attempt to cluster using a + * not-previously-clustered index. */ if (recheck && !cluster_rel_recheck(cmd, OldHeap, indexOid, save_userid, - params->options)) + lmode, params->options)) goto out; /* @@ -353,6 +567,12 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, errmsg("cannot execute %s on a shared catalog", RepackCommandAsString(cmd))); + /* + * The CONCURRENTLY case should have been rejected earlier because it does + * not support system catalogs. + */ + Assert(!(OldHeap->rd_rel->relisshared && concurrent)); + /* * Don't process temp tables of other backends ... their local buffer * manager is not going to cope. @@ -374,7 +594,7 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, if (OidIsValid(indexOid)) { /* verify the index is good and lock it */ - check_index_is_clusterable(OldHeap, indexOid, AccessExclusiveLock); + check_index_is_clusterable(OldHeap, indexOid, lmode); /* also open it */ index = index_open(indexOid, NoLock); } @@ -409,7 +629,9 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, if (OldHeap->rd_rel->relkind == RELKIND_MATVIEW && !RelationIsPopulated(OldHeap)) { - relation_close(OldHeap, AccessExclusiveLock); + if (index) + index_close(index, lmode); + relation_close(OldHeap, lmode); goto out; } @@ -422,11 +644,34 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, * invalid, because we move tuples around. Promote them to relation * locks. Predicate locks on indexes will be promoted when they are * reindexed. + * + * During concurrent processing, the heap as well as its indexes stay in + * operation, so we postpone this step until they are locked using + * AccessExclusiveLock near the end of the processing. */ - TransferPredicateLocksToHeapRelation(OldHeap); + if (!concurrent) + TransferPredicateLocksToHeapRelation(OldHeap); /* rebuild_relation does all the dirty work */ - rebuild_relation(OldHeap, index, verbose); + PG_TRY(); + { + rebuild_relation(OldHeap, index, verbose, ident_idx); + } + PG_FINALLY(); + { + if (concurrent) + { + /* + * Since during normal operation the worker was already asked to + * exit, stopping it explicitly is especially important on ERROR. + * However it still seems a good practice to make sure that the + * worker never survives the REPACK command. + */ + stop_repack_decoding_worker(); + } + } + PG_END_TRY(); + /* rebuild_relation closes OldHeap, and index if valid */ out: @@ -445,14 +690,14 @@ out: */ static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, - Oid userid, int options) + Oid userid, LOCKMODE lmode, int options) { Oid tableOid = RelationGetRelid(OldHeap); /* Check that the user still has privileges for the relation */ if (!repack_is_permitted_for_relation(cmd, tableOid, userid)) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } @@ -466,7 +711,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, */ if (RELATION_IS_OTHER_TEMP(OldHeap)) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } @@ -477,7 +722,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, */ if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid))) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } @@ -488,7 +733,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, if ((options & CLUOPT_RECHECK_ISCLUSTERED) != 0 && !get_index_isclustered(indexOid)) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } } @@ -500,7 +745,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, * Verify that the specified heap and index are valid to cluster on * * Side effect: obtains lock on the index. The caller may - * in some cases already have AccessExclusiveLock on the table, but + * in some cases already have a lock of the same strength on the table, but * not in all cases so we can't rely on the table-level lock for * protection here. */ @@ -626,17 +871,94 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal) } /* - * rebuild_relation: rebuild an existing relation in index or physical order + * Check if the CONCURRENTLY option is legal for the relation. * - * OldHeap: table to rebuild. - * index: index to cluster by, or NULL to rewrite in physical order. - * - * On entry, heap and index (if one is given) must be open, and - * AccessExclusiveLock held on them. - * On exit, they are closed, but locks on them are not released. + * *Ident_idx_p receives OID of the identity index. */ static void -rebuild_relation(Relation OldHeap, Relation index, bool verbose) +check_repack_concurrently_requirements(Relation rel, Oid *ident_idx_p) +{ + char relpersistence, + replident; + Oid ident_idx; + + /* Data changes in system relations are not logically decoded. */ + if (IsCatalogRelation(rel)) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot repack relation \"%s\"", + RelationGetRelationName(rel)), + errhint("REPACK CONCURRENTLY is not supported for catalog relations.")); + + /* + * reorderbuffer.c does not seem to handle processing of TOAST relation + * alone. + */ + if (IsToastRelation(rel)) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot repack relation \"%s\"", + RelationGetRelationName(rel)), + errhint("REPACK CONCURRENTLY is not supported for TOAST relations, unless the main relation is repacked too.")); + + relpersistence = rel->rd_rel->relpersistence; + if (relpersistence != RELPERSISTENCE_PERMANENT) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot repack relation \"%s\"", + RelationGetRelationName(rel)), + errhint("REPACK CONCURRENTLY is only allowed for permanent relations.")); + + /* With NOTHING, WAL does not contain the old tuple. */ + replident = rel->rd_rel->relreplident; + if (replident == REPLICA_IDENTITY_NOTHING) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot repack relation \"%s\"", + RelationGetRelationName(rel)), + errhint("Relation \"%s\" has insufficient replication identity.", + RelationGetRelationName(rel))); + + /* + * If the identity index is not set due to replica identity being, PK + * might exist. + */ + ident_idx = RelationGetReplicaIndex(rel); + if (!OidIsValid(ident_idx) && OidIsValid(rel->rd_pkindex)) + ident_idx = rel->rd_pkindex; + if (!OidIsValid(ident_idx)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot process relation \"%s\"", + RelationGetRelationName(rel)), + errhint("Relation \"%s\" has no identity index.", + RelationGetRelationName(rel))); + + *ident_idx_p = ident_idx; +} + + +/* + * rebuild_relation: rebuild an existing relation in index or physical order + * + * OldHeap: table to rebuild. See cluster_rel() for comments on the required + * lock strength. + * + * index: index to cluster by, or NULL to rewrite in physical order. + * + * ident_idx: identity index, to handle replaying of concurrent data changes + * to the new heap. InvalidOid if there's no CONCURRENTLY option. + * + * On entry, heap and index (if one is given) must be open, and the + * appropriate lock held on them -- AccessExclusiveLock for exclusive + * processing and ShareUpdateExclusiveLock for concurrent processing. + * + * On exit, they are closed, but still locked with AccessExclusiveLock. + * (The function handles the lock upgrade if 'concurrent' is true.) + */ +static void +rebuild_relation(Relation OldHeap, Relation index, bool verbose, + Oid ident_idx) { Oid tableOid = RelationGetRelid(OldHeap); Oid accessMethod = OldHeap->rd_rel->relam; @@ -644,13 +966,55 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose) Oid OIDNewHeap; Relation NewHeap; char relpersistence; - bool is_system_catalog; bool swap_toast_by_content; TransactionId frozenXid; MultiXactId cutoffMulti; + bool concurrent = OidIsValid(ident_idx); + Snapshot snapshot = NULL; +#if USE_ASSERT_CHECKING + LOCKMODE lmode; - Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false) && - (index == NULL || CheckRelationLockedByMe(index, AccessExclusiveLock, false))); + lmode = concurrent ? ShareUpdateExclusiveLock : AccessExclusiveLock; + + Assert(CheckRelationLockedByMe(OldHeap, lmode, false)); + Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false)); +#endif + + if (concurrent) + { + /* + * The worker needs to be member of the locking group we're the leader + * of. We ought to become the leader before the worker starts. The + * worker will join the group as soon as it starts. + * + * This is to make sure that the deadlock described below is + * detectable by deadlock.c: if the worker waits for a transaction to + * complete and we are waiting for the worker output, then effectively + * we (i.e. this backend) are waiting for that transaction. + */ + BecomeLockGroupLeader(); + + /* + * Start the worker that decodes data changes applied while we're + * copying the table contents. + * + * Note that the worker has to wait for all transactions with XID + * already assigned to finish. If some of those transactions is + * waiting for a lock conflicting with ShareUpdateExclusiveLock on our + * table (e.g. it runs CREATE INDEX), we can end up in a deadlock. + * Not sure this risk is worth unlocking/locking the table (and its + * clustering index) and checking again if it's still eligible for + * REPACK CONCURRENTLY. + */ + start_repack_decoding_worker(tableOid); + + /* + * Wait until the worker has the initial snapshot and retrieve it. + */ + snapshot = get_initial_snapshot(decoding_worker); + + PushActiveSnapshot(snapshot); + } /* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */ if (index != NULL) @@ -658,7 +1022,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose) /* Remember info about rel before closing OldHeap */ relpersistence = OldHeap->rd_rel->relpersistence; - is_system_catalog = IsSystemRelation(OldHeap); /* * Create the transient table that will receive the re-ordered data. @@ -674,30 +1037,59 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose) NewHeap = table_open(OIDNewHeap, NoLock); /* Copy the heap data into the new table in the desired order */ - copy_table_data(NewHeap, OldHeap, index, verbose, + copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); + /* The historic snapshot won't be needed anymore. */ + if (snapshot) + { + PopActiveSnapshot(); + UpdateActiveSnapshotCommandId(); + } - /* Close relcache entries, but keep lock until transaction commit */ - table_close(OldHeap, NoLock); - if (index) - index_close(index, NoLock); + if (concurrent) + { + Assert(!swap_toast_by_content); - /* - * Close the new relation so it can be dropped as soon as the storage is - * swapped. The relation is not visible to others, so no need to unlock it - * explicitly. - */ - table_close(NewHeap, NoLock); + /* + * Close the index, but keep the lock. Both heaps will be closed by + * the following call. + */ + if (index) + index_close(index, NoLock); - /* - * Swap the physical files of the target and transient tables, then - * rebuild the target's indexes and throw away the transient table. - */ - finish_heap_swap(tableOid, OIDNewHeap, is_system_catalog, - swap_toast_by_content, false, true, - frozenXid, cutoffMulti, - relpersistence); + rebuild_relation_finish_concurrent(NewHeap, OldHeap, ident_idx, + frozenXid, cutoffMulti); + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_FINAL_CLEANUP); + } + else + { + bool is_system_catalog = IsSystemRelation(OldHeap); + + /* Close relcache entries, but keep lock until transaction commit */ + table_close(OldHeap, NoLock); + if (index) + index_close(index, NoLock); + + /* + * Close the new relation so it can be dropped as soon as the storage + * is swapped. The relation is not visible to others, so no need to + * unlock it explicitly. + */ + table_close(NewHeap, NoLock); + + /* + * Swap the physical files of the target and transient tables, then + * rebuild the target's indexes and throw away the transient table. + */ + finish_heap_swap(tableOid, OIDNewHeap, is_system_catalog, + swap_toast_by_content, false, true, + true, /* reindex */ + frozenXid, cutoffMulti, + relpersistence); + } } @@ -832,15 +1224,18 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod, /* * Do the physical copying of table data. * + * 'snapshot' and 'decoding_ctx': see table_relation_copy_for_cluster(). Pass + * iff concurrent processing is required. + * * There are three output parameters: * *pSwapToastByContent is set true if toast tables must be swapped by content. * *pFreezeXid receives the TransactionId used as freeze cutoff point. * *pCutoffMulti receives the MultiXactId used as a cutoff point. */ static void -copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verbose, - bool *pSwapToastByContent, TransactionId *pFreezeXid, - MultiXactId *pCutoffMulti) +copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, + Snapshot snapshot, bool verbose, bool *pSwapToastByContent, + TransactionId *pFreezeXid, MultiXactId *pCutoffMulti) { Relation relRelation; HeapTuple reltup; @@ -857,6 +1252,10 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb int elevel = verbose ? INFO : DEBUG2; PGRUsage ru0; char *nspname; + bool concurrent = snapshot != NULL; + LOCKMODE lmode; + + lmode = concurrent ? ShareUpdateExclusiveLock : AccessExclusiveLock; pg_rusage_init(&ru0); @@ -885,7 +1284,7 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb * will be held till end of transaction. */ if (OldHeap->rd_rel->reltoastrelid) - LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock); + LockRelationOid(OldHeap->rd_rel->reltoastrelid, lmode); /* * If both tables have TOAST tables, perform toast swap by content. It is @@ -894,7 +1293,8 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb * swap by links. This is okay because swap by content is only essential * for system catalogs, and we don't support schema changes for them. */ - if (OldHeap->rd_rel->reltoastrelid && NewHeap->rd_rel->reltoastrelid) + if (OldHeap->rd_rel->reltoastrelid && NewHeap->rd_rel->reltoastrelid && + !concurrent) { *pSwapToastByContent = true; @@ -915,6 +1315,10 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb * follow the toast pointers to the wrong place. (It would actually * work for values copied over from the old toast table, but not for * any values that we toast which were previously not toasted.) + * + * This would not work with CONCURRENTLY because we may need to delete + * TOASTed tuples from the new heap. With this hack, we'd delete them + * from the old heap. */ NewHeap->rd_toastoid = OldHeap->rd_rel->reltoastrelid; } @@ -990,7 +1394,8 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb * values (e.g. because the AM doesn't use freezing). */ table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort, - cutoffs.OldestXmin, &cutoffs.FreezeLimit, + cutoffs.OldestXmin, snapshot, + &cutoffs.FreezeLimit, &cutoffs.MultiXactCutoff, &num_tuples, &tups_vacuumed, &tups_recently_dead); @@ -999,7 +1404,11 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb *pFreezeXid = cutoffs.FreezeLimit; *pCutoffMulti = cutoffs.MultiXactCutoff; - /* Reset rd_toastoid just to be tidy --- it shouldn't be looked at again */ + /* + * Reset rd_toastoid just to be tidy --- it shouldn't be looked at again. + * In the CONCURRENTLY case, we need to set it again before applying the + * concurrent changes. + */ NewHeap->rd_toastoid = InvalidOid; num_pages = RelationGetNumberOfBlocks(NewHeap); @@ -1457,14 +1866,13 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, bool swap_toast_by_content, bool check_constraints, bool is_internal, + bool reindex, TransactionId frozenXid, MultiXactId cutoffMulti, char newrelpersistence) { ObjectAddress object; Oid mapped_tables[4]; - int reindex_flags; - ReindexParams reindex_params = {0}; int i; /* Report that we are now swapping relation files */ @@ -1490,39 +1898,47 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, if (is_system_catalog) CacheInvalidateCatalog(OIDOldHeap); - /* - * Rebuild each index on the relation (but not the toast table, which is - * all-new at this point). It is important to do this before the DROP - * step because if we are processing a system catalog that will be used - * during DROP, we want to have its indexes available. There is no - * advantage to the other order anyway because this is all transactional, - * so no chance to reclaim disk space before commit. We do not need a - * final CommandCounterIncrement() because reindex_relation does it. - * - * Note: because index_build is called via reindex_relation, it will never - * set indcheckxmin true for the indexes. This is OK even though in some - * sense we are building new indexes rather than rebuilding existing ones, - * because the new heap won't contain any HOT chains at all, let alone - * broken ones, so it can't be necessary to set indcheckxmin. - */ - reindex_flags = REINDEX_REL_SUPPRESS_INDEX_USE; - if (check_constraints) - reindex_flags |= REINDEX_REL_CHECK_CONSTRAINTS; + if (reindex) + { + int reindex_flags; + ReindexParams reindex_params = {0}; - /* - * Ensure that the indexes have the same persistence as the parent - * relation. - */ - if (newrelpersistence == RELPERSISTENCE_UNLOGGED) - reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED; - else if (newrelpersistence == RELPERSISTENCE_PERMANENT) - reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT; + /* + * Rebuild each index on the relation (but not the toast table, which + * is all-new at this point). It is important to do this before the + * DROP step because if we are processing a system catalog that will + * be used during DROP, we want to have its indexes available. There + * is no advantage to the other order anyway because this is all + * transactional, so no chance to reclaim disk space before commit. We + * do not need a final CommandCounterIncrement() because + * reindex_relation does it. + * + * Note: because index_build is called via reindex_relation, it will + * never set indcheckxmin true for the indexes. This is OK even + * though in some sense we are building new indexes rather than + * rebuilding existing ones, because the new heap won't contain any + * HOT chains at all, let alone broken ones, so it can't be necessary + * to set indcheckxmin. + */ + reindex_flags = REINDEX_REL_SUPPRESS_INDEX_USE; + if (check_constraints) + reindex_flags |= REINDEX_REL_CHECK_CONSTRAINTS; - /* Report that we are now reindexing relations */ - pgstat_progress_update_param(PROGRESS_REPACK_PHASE, - PROGRESS_REPACK_PHASE_REBUILD_INDEX); + /* + * Ensure that the indexes have the same persistence as the parent + * relation. + */ + if (newrelpersistence == RELPERSISTENCE_UNLOGGED) + reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED; + else if (newrelpersistence == RELPERSISTENCE_PERMANENT) + reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT; - reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params); + /* Report that we are now reindexing relations */ + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_REBUILD_INDEX); + + reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params); + } /* Report that we are now doing clean up */ pgstat_progress_update_param(PROGRESS_REPACK_PHASE, @@ -1566,6 +1982,17 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, object.objectId = OIDNewHeap; object.objectSubId = 0; + if (!reindex) + { + /* + * Make sure the changes in pg_class are visible. This is especially + * important if !swap_toast_by_content, so that the correct TOAST + * relation is dropped. (reindex_relation() above did not help in this + * case)) + */ + CommandCounterIncrement(); + } + /* * The new relation is local to our transaction and we know nothing * depends on it, so DROP_RESTRICT should be OK. @@ -1605,7 +2032,7 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, /* Get the associated valid index to be renamed */ toastidx = toast_get_valid_index(newrel->rd_rel->reltoastrelid, - NoLock); + AccessExclusiveLock); /* rename the toast table ... */ snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u", @@ -1876,7 +2303,8 @@ repack_is_permitted_for_relation(RepackCommand cmd, Oid relid, Oid userid) * case, if an index name is given, it's up to the caller to resolve it. */ static Relation -process_single_relation(RepackStmt *stmt, ClusterParams *params) +process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, + ClusterParams *params) { Relation rel; Oid tableOid; @@ -1893,13 +2321,9 @@ process_single_relation(RepackStmt *stmt, ClusterParams *params) errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("ANALYZE option must be specified when a column list is provided")); - /* - * Find, lock, and check permissions on the table. We obtain - * AccessExclusiveLock right away to avoid lock-upgrade hazard in the - * single-transaction case. - */ + /* Find, lock, and check permissions on the table. */ tableOid = RangeVarGetRelidExtended(stmt->relation->relation, - AccessExclusiveLock, + lockmode, 0, RangeVarCallbackMaintainsTable, NULL); @@ -1924,13 +2348,14 @@ process_single_relation(RepackStmt *stmt, ClusterParams *params) return rel; else { - Oid indexOid; + Oid indexOid = InvalidOid; indexOid = determine_clustered_index(rel, stmt->usingindex, stmt->indexname); if (OidIsValid(indexOid)) - check_index_is_clusterable(rel, indexOid, AccessExclusiveLock); - cluster_rel(stmt->command, rel, indexOid, params); + check_index_is_clusterable(rel, indexOid, lockmode); + + cluster_rel(stmt->command, rel, indexOid, params, isTopLevel); /* * Do an analyze, if requested. We close the transaction and start a @@ -2025,3 +2450,1199 @@ RepackCommandAsString(RepackCommand cmd) } return "???"; /* keep compiler quiet */ } + +/* + * Apply all the changes stored in 'file'. + */ +static void +apply_concurrent_changes(BufFile *file, ChangeContext *chgcxt) +{ + ConcurrentChangeKind kind = '\0'; + Relation rel = chgcxt->cc_rel; + TupleTableSlot *spilled_tuple; + TupleTableSlot *old_update_tuple; + TupleTableSlot *ondisk_tuple; + bool have_old_tuple = false; + MemoryContext oldcxt; + + spilled_tuple = MakeSingleTupleTableSlot(RelationGetDescr(rel), + &TTSOpsVirtual); + ondisk_tuple = MakeSingleTupleTableSlot(RelationGetDescr(rel), + table_slot_callbacks(rel)); + old_update_tuple = MakeSingleTupleTableSlot(RelationGetDescr(rel), + &TTSOpsVirtual); + + oldcxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(chgcxt->cc_estate)); + + while (true) + { + size_t nread; + ConcurrentChangeKind prevkind = kind; + + CHECK_FOR_INTERRUPTS(); + + nread = BufFileReadMaybeEOF(file, &kind, 1, true); + if (nread == 0) /* done with the file? */ + break; + + /* + * If this is the old tuple for an update, read it into the tuple slot + * and go to the next one. The update itself will be executed on the + * next iteration, when we receive the NEW tuple. + */ + if (kind == CHANGE_UPDATE_OLD) + { + restore_tuple(file, rel, old_update_tuple); + have_old_tuple = true; + continue; + } + + /* + * Just before an UPDATE or DELETE, we must update the command + * counter, because the change could refer to a tuple that we have + * just inserted; and before an INSERT, we have to do this also if the + * previous command was either update or delete. + * + * With this approach we don't spend so many CCIs for long strings of + * only INSERTs, which can't affect one another. + */ + if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE || + (kind == CHANGE_INSERT && (prevkind == CHANGE_UPDATE_NEW || + prevkind == CHANGE_DELETE))) + { + CommandCounterIncrement(); + UpdateActiveSnapshotCommandId(); + } + + /* + * Now restore the tuple into the slot and execute the change. + */ + restore_tuple(file, rel, spilled_tuple); + + if (kind == CHANGE_INSERT) + { + apply_concurrent_insert(rel, spilled_tuple, chgcxt); + } + else if (kind == CHANGE_DELETE) + { + bool found; + + /* Find the tuple to be deleted */ + found = find_target_tuple(rel, chgcxt, spilled_tuple, ondisk_tuple); + if (!found) + elog(ERROR, "failed to find target tuple"); + apply_concurrent_delete(rel, ondisk_tuple); + } + else if (kind == CHANGE_UPDATE_NEW) + { + TupleTableSlot *key; + bool found; + + if (have_old_tuple) + key = old_update_tuple; + else + key = spilled_tuple; + + /* Find the tuple to be updated or deleted. */ + found = find_target_tuple(rel, chgcxt, key, ondisk_tuple); + if (!found) + elog(ERROR, "failed to find target tuple"); + + /* + * If 'tup' contains TOAST pointers, they point to the old + * relation's toast. Copy the corresponding TOAST pointers for the + * new relation from the existing tuple. (The fact that we + * received a TOAST pointer here implies that the attribute hasn't + * changed.) + */ + adjust_toast_pointers(rel, spilled_tuple, ondisk_tuple); + + apply_concurrent_update(rel, spilled_tuple, ondisk_tuple, chgcxt); + + ExecClearTuple(old_update_tuple); + have_old_tuple = false; + } + else + elog(ERROR, "unrecognized kind of change: %d", kind); + + ResetPerTupleExprContext(chgcxt->cc_estate); + } + + /* Cleanup. */ + ExecDropSingleTupleTableSlot(spilled_tuple); + ExecDropSingleTupleTableSlot(ondisk_tuple); + ExecDropSingleTupleTableSlot(old_update_tuple); + + MemoryContextSwitchTo(oldcxt); +} + +/* + * Apply an insert from the spill of concurrent changes to the new copy of the + * table. + */ +static void +apply_concurrent_insert(Relation rel, TupleTableSlot *slot, + ChangeContext *chgcxt) +{ + /* Put the tuple in the table, but make sure it won't be decoded */ + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + HEAP_INSERT_NO_LOGICAL, NULL); + + /* Update indexes with this new tuple. */ + ExecInsertIndexTuples(chgcxt->cc_rri, + chgcxt->cc_estate, + 0, + slot, + NIL, NULL); + pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, 1); +} + +/* + * Apply an update from the spill of concurrent changes to the new copy of the + * table. + */ +static void +apply_concurrent_update(Relation rel, TupleTableSlot *spilled_tuple, + TupleTableSlot *ondisk_tuple, + ChangeContext *chgcxt) +{ + LockTupleMode lockmode; + TM_FailureData tmfd; + TU_UpdateIndexes update_indexes; + TM_Result res; + + /* + * Carry out the update, skipping logical decoding for it. + */ + res = table_tuple_update(rel, &(ondisk_tuple->tts_tid), spilled_tuple, + GetCurrentCommandId(true), + TABLE_UPDATE_NO_LOGICAL, + InvalidSnapshot, + InvalidSnapshot, + false, + &tmfd, &lockmode, &update_indexes); + if (res != TM_Ok) + ereport(ERROR, + errmsg("failed to apply concurrent UPDATE")); + + if (update_indexes != TU_None) + { + uint32 flags = EIIT_IS_UPDATE; + + if (update_indexes == TU_Summarizing) + flags |= EIIT_ONLY_SUMMARIZING; + ExecInsertIndexTuples(chgcxt->cc_rri, + chgcxt->cc_estate, + flags, + spilled_tuple, + NIL, NULL); + } + + pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_UPDATED, 1); +} + +static void +apply_concurrent_delete(Relation rel, TupleTableSlot *slot) +{ + TM_Result res; + TM_FailureData tmfd; + + /* + * Delete tuple from the new heap, skipping logical decoding for it. + */ + res = table_tuple_delete(rel, &(slot->tts_tid), + GetCurrentCommandId(true), + TABLE_DELETE_NO_LOGICAL, + InvalidSnapshot, InvalidSnapshot, + false, + &tmfd); + + if (res != TM_Ok) + ereport(ERROR, + errmsg("failed to apply concurrent DELETE")); + + pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_DELETED, 1); +} + +/* + * Read tuple from file and put it in the input slot. + * + * External attributes are stored in separate memory chunks, in order to avoid + * exceeding MaxAllocSize - that could happen if the individual attributes are + * smaller than MaxAllocSize but the whole tuple is bigger. + */ +static void +restore_tuple(BufFile *file, Relation relation, TupleTableSlot *slot) +{ + uint32 t_len; + HeapTuple tup; + MemoryContext oldcxt; + int natt_ext; + + oldcxt = MemoryContextSwitchTo(slot->tts_mcxt); + + /* Read the tuple. */ + BufFileReadExact(file, &t_len, sizeof(t_len)); + tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len); + tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE); + BufFileReadExact(file, tup->t_data, t_len); + tup->t_len = t_len; + ItemPointerSetInvalid(&tup->t_self); + tup->t_tableOid = RelationGetRelid(relation); + + /* + * Put the tuple we read in a slot. This deforms it, so that we can hack + * the external attributes in place. + */ + ExecForceStoreHeapTuple(tup, slot, false); + + /* + * Next, read any attributes we stored separately into the tts_values + * array elements expecting them, if any. This matches + * repack_store_change. + */ + BufFileReadExact(file, &natt_ext, sizeof(natt_ext)); + if (natt_ext > 0) + { + TupleDesc desc = slot->tts_tupleDescriptor; + + for (int i = 0; i < desc->natts; i++) + { + CompactAttribute *attr = TupleDescCompactAttr(desc, i); + varlena *varlen; + union + { + alignas(int32) varlena hdr; + char data[sizeof(void *)]; + } chunk_header; + void *value; + Size varlensz; + + if (attr->attisdropped || attr->attlen != -1) + continue; + if (slot_attisnull(slot, i + 1)) + continue; + varlen = (varlena *) DatumGetPointer(slot->tts_values[i]); + if (!VARATT_IS_EXTERNAL(varlen)) + continue; + slot_getsomeattrs(slot, i + 1); + + BufFileReadExact(file, &chunk_header, VARHDRSZ); + varlensz = VARSIZE_ANY(&chunk_header); + + value = palloc(varlensz); + SET_VARSIZE(value, VARSIZE_ANY(&chunk_header)); + BufFileReadExact(file, (char *) value + VARHDRSZ, varlensz - VARHDRSZ); + + slot->tts_values[i] = PointerGetDatum(value); + natt_ext--; + } + } + + MemoryContextSwitchTo(oldcxt); +} + +/* + * Adjust 'dest' replacing any EXTERNAL_ONDISK toast pointers with the + * corresponding ones from 'src'. + */ +static void +adjust_toast_pointers(Relation relation, TupleTableSlot *dest, TupleTableSlot *src) +{ + TupleDesc desc = dest->tts_tupleDescriptor; + + for (int i = 0; i < desc->natts; i++) + { + CompactAttribute *attr = TupleDescCompactAttr(desc, i); + varlena *varlena_dst; + + if (attr->attisdropped) + continue; + if (attr->attlen != -1) + continue; + if (slot_attisnull(dest, i + 1)) + continue; + + slot_getsomeattrs(dest, i + 1); + + varlena_dst = (varlena *) DatumGetPointer(dest->tts_values[i]); + if (!VARATT_IS_EXTERNAL_ONDISK(varlena_dst)) + continue; + slot_getsomeattrs(src, i + 1); + + /* + * XXX We simply replace the pointer to the Datum from the other one, + * which is probably bogus. + */ + dest->tts_values[i] = src->tts_values[i]; + } +} + +/* + * Find the tuple to be updated or deleted by the given data change, whose + * tuple has already been loaded into locator. + * + * If the tuple is found, put it in retrieved and return true. If the tuple is + * not found, return false. + */ +static bool +find_target_tuple(Relation rel, ChangeContext *chgcxt, TupleTableSlot *locator, + TupleTableSlot *retrieved) +{ + Form_pg_index idx = chgcxt->cc_ident_index->rd_index; + IndexScanDesc scan; + bool retval; + + /* + * Scan key is passed by caller, so it does not have to be constructed + * multiple times. Key entries have all fields initialized, except for + * sk_argument. + * + * Use the incoming tuple to finalize the scan key. + */ + for (int i = 0; i < chgcxt->cc_ident_key_nentries; i++) + { + ScanKey entry = &chgcxt->cc_ident_key[i]; + AttrNumber attno = idx->indkey.values[i]; + + entry->sk_argument = locator->tts_values[attno - 1]; + Assert(!locator->tts_isnull[attno - 1]); + } + + /* XXX no instrumentation for now */ + scan = index_beginscan(rel, chgcxt->cc_ident_index, GetActiveSnapshot(), + NULL, chgcxt->cc_ident_key_nentries, 0, 0); + index_rescan(scan, chgcxt->cc_ident_key, chgcxt->cc_ident_key_nentries, NULL, 0); + retval = index_getnext_slot(scan, ForwardScanDirection, retrieved); + index_endscan(scan); + + return retval; +} + +/* + * Decode and apply concurrent changes, up to (and including) the record whose + * LSN is 'end_of_wal'. + * + * XXX the names "process_concurrent_changes" and "apply_concurrent_changes" + * are far too similar to each other. + */ +static void +process_concurrent_changes(XLogRecPtr end_of_wal, ChangeContext *chgcxt, bool done) +{ + DecodingWorkerShared *shared; + char fname[MAXPGPATH]; + BufFile *file; + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_CATCH_UP); + + /* Ask the worker for the file. */ + shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg); + SpinLockAcquire(&shared->mutex); + shared->lsn_upto = end_of_wal; + shared->done = done; + SpinLockRelease(&shared->mutex); + + /* + * The worker needs to finish processing of the current WAL record. Even + * if it's idle, it'll need to close the output file. Thus we're likely to + * wait, so prepare for sleep. + */ + ConditionVariablePrepareToSleep(&shared->cv); + for (;;) + { + int last_exported; + + SpinLockAcquire(&shared->mutex); + last_exported = shared->last_exported; + SpinLockRelease(&shared->mutex); + + /* + * Has the worker exported the file we are waiting for? + */ + if (last_exported == chgcxt->cc_file_seq) + break; + + ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT); + } + ConditionVariableCancelSleep(); + + /* Open the file. */ + DecodingWorkerFileName(fname, shared->relid, chgcxt->cc_file_seq); + file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false); + apply_concurrent_changes(file, chgcxt); + + BufFileClose(file); + + /* Get ready for the next file. */ + chgcxt->cc_file_seq++; +} + +/* + * Initialize the ChangeContext struct for the given relation, with + * the given index as identity index. + */ +static void +initialize_change_context(ChangeContext *chgcxt, + Relation relation, Oid ident_index_id) +{ + chgcxt->cc_rel = relation; + + /* Only initialize fields needed by ExecInsertIndexTuples(). */ + chgcxt->cc_estate = CreateExecutorState(); + + chgcxt->cc_rri = (ResultRelInfo *) palloc(sizeof(ResultRelInfo)); + InitResultRelInfo(chgcxt->cc_rri, relation, 0, 0, 0); + ExecOpenIndices(chgcxt->cc_rri, false); + + /* + * The table's relcache entry already has the relcache entry for the + * identity index; find that. + */ + chgcxt->cc_ident_index = NULL; + for (int i = 0; i < chgcxt->cc_rri->ri_NumIndices; i++) + { + Relation ind_rel; + + ind_rel = chgcxt->cc_rri->ri_IndexRelationDescs[i]; + if (ind_rel->rd_id == ident_index_id) + { + chgcxt->cc_ident_index = ind_rel; + break; + } + } + if (chgcxt->cc_ident_index == NULL) + elog(ERROR, "failed to find identity index"); + + /* Set up for scanning said identity index */ + { + Form_pg_index indexForm; + + indexForm = chgcxt->cc_ident_index->rd_index; + chgcxt->cc_ident_key_nentries = indexForm->indnkeyatts; + chgcxt->cc_ident_key = (ScanKey) palloc_array(ScanKeyData, indexForm->indnkeyatts); + for (int i = 0; i < indexForm->indnkeyatts; i++) + { + ScanKey entry; + Oid opfamily, + opcintype, + opno, + opcode; + + entry = &chgcxt->cc_ident_key[i]; + + opfamily = chgcxt->cc_ident_index->rd_opfamily[i]; + opcintype = chgcxt->cc_ident_index->rd_opcintype[i]; + opno = get_opfamily_member(opfamily, opcintype, opcintype, + BTEqualStrategyNumber); + if (!OidIsValid(opno)) + elog(ERROR, "failed to find = operator for type %u", opcintype); + opcode = get_opcode(opno); + if (!OidIsValid(opcode)) + elog(ERROR, "failed to find = operator for operator %u", opno); + + /* Initialize everything but argument. */ + ScanKeyInit(entry, + i + 1, + BTEqualStrategyNumber, opcode, + (Datum) NULL); + entry->sk_collation = chgcxt->cc_ident_index->rd_indcollation[i]; + } + } + + chgcxt->cc_file_seq = WORKER_FILE_SNAPSHOT + 1; +} + +/* + * Free up resources taken by a ChangeContext. + */ +static void +release_change_context(ChangeContext *chgcxt) +{ + ExecCloseIndices(chgcxt->cc_rri); + FreeExecutorState(chgcxt->cc_estate); + /* XXX are these pfrees necessary? */ + pfree(chgcxt->cc_rri); + pfree(chgcxt->cc_ident_key); +} + +/* + * The final steps of rebuild_relation() for concurrent processing. + * + * On entry, NewHeap is locked in AccessExclusiveLock mode. OldHeap and its + * clustering index (if one is passed) are still locked in a mode that allows + * concurrent data changes. On exit, both tables and their indexes are closed, + * but locked in AccessExclusiveLock mode. + */ +static void +rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, + Oid identIdx, TransactionId frozenXid, + MultiXactId cutoffMulti) +{ + LOCKMODE lockmode_old PG_USED_FOR_ASSERTS_ONLY; + List *ind_oids_new; + Oid old_table_oid = RelationGetRelid(OldHeap); + Oid new_table_oid = RelationGetRelid(NewHeap); + List *ind_oids_old = RelationGetIndexList(OldHeap); + ListCell *lc, + *lc2; + char relpersistence; + bool is_system_catalog; + Oid ident_idx_new; + XLogRecPtr end_of_wal; + List *indexrels; + ChangeContext chgcxt; + + /* Like in cluster_rel(). */ + lockmode_old = ShareUpdateExclusiveLock; + Assert(CheckRelationLockedByMe(OldHeap, lockmode_old, false)); + /* This is expected from the caller. */ + Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false)); + + /* + * Unlike the exclusive case, we build new indexes for the new relation + * rather than swapping the storage and reindexing the old relation. The + * point is that the index build can take some time, so we do it before we + * get AccessExclusiveLock on the old heap and therefore we cannot swap + * the heap storage yet. + * + * index_create() will lock the new indexes using AccessExclusiveLock - no + * need to change that. At the same time, we use ShareUpdateExclusiveLock + * to lock the existing indexes - that should be enough to prevent others + * from changing them while we're repacking the relation. The lock on + * table should prevent others from changing the index column list, but + * might not be enough for commands like ALTER INDEX ... SET ... (Those + * are not necessarily dangerous, but can make user confused if the + * changes they do get lost due to REPACK.) + */ + ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old); + + /* + * The identity index in the new relation appears in the same relative + * position as the corresponding index in the old relation. Find it. + */ + ident_idx_new = InvalidOid; + foreach_oid(ind_old, ind_oids_old) + { + if (identIdx == ind_old) + { + ident_idx_new = list_nth_oid(ind_oids_new, + foreach_current_index(ind_old)); + break; + } + } + if (!OidIsValid(ident_idx_new)) + elog(ERROR, "could not find index matching \"%s\" at the new relation", + get_rel_name(identIdx)); + + /* Gather information to apply concurrent changes. */ + initialize_change_context(&chgcxt, NewHeap, ident_idx_new); + + /* + * During testing, wait for another backend to perform concurrent data + * changes which we will process below. + */ + INJECTION_POINT("repack-concurrently-before-lock", NULL); + + /* + * Flush all WAL records inserted so far (possibly except for the last + * incomplete page; see GetInsertRecPtr), to minimize the amount of data + * we need to flush while holding exclusive lock on the source table. + */ + XLogFlush(GetXLogInsertEndRecPtr()); + end_of_wal = GetFlushRecPtr(NULL); + + /* + * Apply concurrent changes first time, to minimize the time we need to + * hold AccessExclusiveLock. (Quite some amount of WAL could have been + * written during the data copying and index creation.) + */ + process_concurrent_changes(end_of_wal, &chgcxt, false); + + /* + * Acquire AccessExclusiveLock on the table, its TOAST relation (if there + * is one), all its indexes, so that we can swap the files. + */ + LockRelationOid(old_table_oid, AccessExclusiveLock); + + /* + * Lock all indexes now, not only the clustering one: all indexes need to + * have their files swapped. While doing that, store their relation + * references in a zero-terminated array, to handle predicate locks below. + */ + indexrels = NIL; + foreach_oid(ind_oid, ind_oids_old) + { + Relation index; + + index = index_open(ind_oid, AccessExclusiveLock); + + /* + * Some things about the index may have changed before we locked the + * index, such as ALTER INDEX RENAME. We don't need to do anything + * here to absorb those changes in the new index. + */ + indexrels = lappend(indexrels, index); + } + + /* + * Lock the OldHeap's TOAST relation exclusively - again, the lock is + * needed to swap the files. + */ + if (OidIsValid(OldHeap->rd_rel->reltoastrelid)) + LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock); + + /* + * Tuples and pages of the old heap will be gone, but the heap will stay. + */ + TransferPredicateLocksToHeapRelation(OldHeap); + foreach_ptr(RelationData, index, indexrels) + { + TransferPredicateLocksToHeapRelation(index); + index_close(index, NoLock); + } + list_free(indexrels); + + /* + * Flush WAL again, to make sure that all changes committed while we were + * waiting for the exclusive lock are available for decoding. + */ + XLogFlush(GetXLogInsertEndRecPtr()); + end_of_wal = GetFlushRecPtr(NULL); + + /* + * Apply the concurrent changes again. Indicate that the decoding worker + * won't be needed anymore. + */ + process_concurrent_changes(end_of_wal, &chgcxt, true); + + /* Remember info about rel before closing OldHeap */ + relpersistence = OldHeap->rd_rel->relpersistence; + is_system_catalog = IsSystemRelation(OldHeap); + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_SWAP_REL_FILES); + + /* + * Even ShareUpdateExclusiveLock should have prevented others from + * creating / dropping indexes (even using the CONCURRENTLY option), so we + * do not need to check whether the lists match. + */ + forboth(lc, ind_oids_old, lc2, ind_oids_new) + { + Oid ind_old = lfirst_oid(lc); + Oid ind_new = lfirst_oid(lc2); + Oid mapped_tables[4] = {0}; + + swap_relation_files(ind_old, ind_new, + (old_table_oid == RelationRelationId), + false, /* swap_toast_by_content */ + true, + InvalidTransactionId, + InvalidMultiXactId, + mapped_tables); + +#ifdef USE_ASSERT_CHECKING + + /* + * Concurrent processing is not supported for system relations, so + * there should be no mapped tables. + */ + for (int i = 0; i < 4; i++) + Assert(!OidIsValid(mapped_tables[i])); +#endif + } + + /* The new indexes must be visible for deletion. */ + CommandCounterIncrement(); + + /* Close the old heap but keep lock until transaction commit. */ + table_close(OldHeap, NoLock); + /* Close the new heap. (We didn't have to open its indexes). */ + table_close(NewHeap, NoLock); + + /* Cleanup what we don't need anymore. (And close the identity index.) */ + release_change_context(&chgcxt); + + /* + * Swap the relations and their TOAST relations and TOAST indexes. This + * also drops the new relation and its indexes. + * + * (System catalogs are currently not supported.) + */ + Assert(!is_system_catalog); + finish_heap_swap(old_table_oid, new_table_oid, + is_system_catalog, + false, /* swap_toast_by_content */ + false, + true, + false, /* reindex */ + frozenXid, cutoffMulti, + relpersistence); +} + +/* + * Build indexes on NewHeap according to those on OldHeap. + * + * OldIndexes is the list of index OIDs on OldHeap. The contained indexes end + * up locked using ShareUpdateExclusiveLock. + * + * A list of OIDs of the corresponding indexes created on NewHeap is + * returned. The order of items does match, so we can use these arrays to swap + * index storage. + */ +static List * +build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes) +{ + List *result = NIL; + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_REBUILD_INDEX); + + foreach_oid(oldindex, OldIndexes) + { + Oid newindex; + char *newName; + Relation ind; + + ind = index_open(oldindex, ShareUpdateExclusiveLock); + + newName = ChooseRelationName(get_rel_name(oldindex), + NULL, + "repacknew", + get_rel_namespace(ind->rd_index->indrelid), + false); + newindex = index_create_copy(NewHeap, false, oldindex, + ind->rd_rel->reltablespace, newName); + copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap)); + result = lappend_oid(result, newindex); + + index_close(ind, NoLock); + } + + return result; +} + +/* + * Create a transient copy of a constraint -- supported by a transient + * copy of the index that supports the original constraint. + * + * When repacking a table that contains exclusion constraints, the executor + * relies on these constraints being properly catalogued. These copies are + * to support that. + * + * We don't need the constraints for anything else (the original constraints + * will be there once repack completes), so we add pg_depend entries so that + * the are dropped when the transient table is dropped. + */ +static void +copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) +{ + ScanKeyData skey; + Relation rel; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(ConstraintRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + /* + * Retrieve the constraints supported by the old index and create an + * identical one that points to the new index. + */ + ScanKeyInit(&skey, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_index->rd_index->indrelid)); + scan = systable_beginscan(rel, ConstraintRelidTypidNameIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(tup); + Oid oid; + Datum values[Natts_pg_constraint] = {0}; + bool nulls[Natts_pg_constraint] = {0}; + bool replaces[Natts_pg_constraint] = {0}; + HeapTuple new_tup; + ObjectAddress objcon; + + if (conform->conindid != RelationGetRelid(old_index)) + continue; + + oid = GetNewOidWithIndex(rel, ConstraintOidIndexId, + Anum_pg_constraint_oid); + values[Anum_pg_constraint_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_constraint_oid - 1] = true; + values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_constraint_conrelid - 1] = true; + values[Anum_pg_constraint_conindid - 1] = ObjectIdGetDatum(new_index_id); + replaces[Anum_pg_constraint_conindid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objcon, ConstraintRelationId, oid); + recordDependencyOn(&objcon, &objrel, DEPENDENCY_AUTO); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + +/* + * Try to start a background worker to perform logical decoding of data + * changes applied to relation while REPACK CONCURRENTLY is copying its + * contents to a new table. + */ +static void +start_repack_decoding_worker(Oid relid) +{ + Size size; + dsm_segment *seg; + DecodingWorkerShared *shared; + shm_mq *mq; + shm_mq_handle *mqh; + BackgroundWorker bgw; + + /* Setup shared memory. */ + size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) + + BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE); + seg = dsm_create(size, 0); + shared = (DecodingWorkerShared *) dsm_segment_address(seg); + shared->lsn_upto = InvalidXLogRecPtr; + shared->done = false; + SharedFileSetInit(&shared->sfs, seg); + shared->last_exported = -1; + SpinLockInit(&shared->mutex); + shared->dbid = MyDatabaseId; + + /* + * This is the UserId set in cluster_rel(). Security context shouldn't be + * needed for decoding worker. + */ + shared->roleid = GetUserId(); + shared->relid = relid; + ConditionVariableInit(&shared->cv); + shared->backend_proc = MyProc; + shared->backend_pid = MyProcPid; + shared->backend_proc_number = MyProcNumber; + + mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue), + REPACK_ERROR_QUEUE_SIZE); + shm_mq_set_receiver(mq, MyProc); + mqh = shm_mq_attach(mq, seg, NULL); + + memset(&bgw, 0, sizeof(bgw)); + snprintf(bgw.bgw_name, BGW_MAXLEN, + "REPACK decoding worker for relation \"%s\"", + get_rel_name(relid)); + snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker"); + bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | + BGWORKER_BACKEND_DATABASE_CONNECTION; + bgw.bgw_start_time = BgWorkerStart_RecoveryFinished; + bgw.bgw_restart_time = BGW_NEVER_RESTART; + snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres"); + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain"); + bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg)); + bgw.bgw_notify_pid = MyProcPid; + + decoding_worker = palloc0_object(DecodingWorker); + if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle)) + ereport(ERROR, + errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), + errmsg("out of background worker slots"), + errhint("You might need to increase \"%s\".", "max_worker_processes")); + + decoding_worker->seg = seg; + decoding_worker->error_mqh = mqh; + + /* + * The decoding setup must be done before the caller can have XID assigned + * for any reason, otherwise the worker might end up in a deadlock, + * waiting for the caller's transaction to end. Therefore wait here until + * the worker indicates that it has the logical decoding initialized. + */ + ConditionVariablePrepareToSleep(&shared->cv); + for (;;) + { + bool initialized; + + SpinLockAcquire(&shared->mutex); + initialized = shared->initialized; + SpinLockRelease(&shared->mutex); + + if (initialized) + break; + + ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT); + } + ConditionVariableCancelSleep(); +} + +/* + * Stop the decoding worker and cleanup the related resources. + * + * The worker stops on its own when it knows there is no more work to do, but + * we need to stop it explicitly at least on ERROR in the launching backend. + */ +static void +stop_repack_decoding_worker(void) +{ + BgwHandleStatus status; + + /* Haven't reached the worker startup? */ + if (decoding_worker == NULL) + return; + + /* Could not register the worker? */ + if (decoding_worker->handle == NULL) + return; + + TerminateBackgroundWorker(decoding_worker->handle); + /* The worker should really exit before the REPACK command does. */ + HOLD_INTERRUPTS(); + status = WaitForBackgroundWorkerShutdown(decoding_worker->handle); + RESUME_INTERRUPTS(); + + if (status == BGWH_POSTMASTER_DIED) + ereport(FATAL, + errcode(ERRCODE_ADMIN_SHUTDOWN), + errmsg("postmaster exited during REPACK command")); + + shm_mq_detach(decoding_worker->error_mqh); + + /* + * If we could not cancel the current sleep due to ERROR, do that before + * we detach from the shared memory the condition variable is located in. + * If we did not, the bgworker ERROR handling code would try and fail + * badly. + */ + ConditionVariableCancelSleep(); + + dsm_detach(decoding_worker->seg); + pfree(decoding_worker); + decoding_worker = NULL; +} + +/* + * Get the initial snapshot from the decoding worker. + */ +static Snapshot +get_initial_snapshot(DecodingWorker *worker) +{ + DecodingWorkerShared *shared; + char fname[MAXPGPATH]; + BufFile *file; + Size snap_size; + char *snap_space; + Snapshot snapshot; + + shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg); + + /* + * The worker needs to initialize the logical decoding, which usually + * takes some time. Therefore it makes sense to prepare for the sleep + * first. + */ + ConditionVariablePrepareToSleep(&shared->cv); + for (;;) + { + int last_exported; + + SpinLockAcquire(&shared->mutex); + last_exported = shared->last_exported; + SpinLockRelease(&shared->mutex); + + /* + * Has the worker exported the file we are waiting for? + */ + if (last_exported == WORKER_FILE_SNAPSHOT) + break; + + ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT); + } + ConditionVariableCancelSleep(); + + /* Read the snapshot from a file. */ + DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT); + file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false); + BufFileReadExact(file, &snap_size, sizeof(snap_size)); + snap_space = (char *) palloc(snap_size); + BufFileReadExact(file, snap_space, snap_size); + BufFileClose(file); + + /* Restore it. */ + snapshot = RestoreSnapshot(snap_space); + pfree(snap_space); + + return snapshot; +} + +/* + * Generate worker's file name into 'fname', which must be of size MAXPGPATH. + * If relations of the same 'relid' happen to be processed at the same time, + * they must be from different databases and therefore different backends must + * be involved. + */ +void +DecodingWorkerFileName(char *fname, Oid relid, uint32 seq) +{ + /* The PID is already present in the fileset name, so we needn't add it */ + snprintf(fname, MAXPGPATH, "%u-%u", relid, seq); +} + +/* + * Handle receipt of an interrupt indicating a repack worker message. + * + * Note: this is called within a signal handler! All we can do is set + * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke + * ProcessRepackMessages(). + */ +void +HandleRepackMessageInterrupt(void) +{ + InterruptPending = true; + RepackMessagePending = true; + SetLatch(MyLatch); +} + +/* + * Process any queued protocol messages received from the repack worker. + */ +void +ProcessRepackMessages(void) +{ + MemoryContext oldcontext; + static MemoryContext hpm_context = NULL; + + /* + * Nothing to do if we haven't launched the worker yet or have already + * terminated it. + */ + if (decoding_worker == NULL) + return; + + /* + * This is invoked from ProcessInterrupts(), and since some of the + * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential + * for recursive calls if more signals are received while this runs. It's + * unclear that recursive entry would be safe, and it doesn't seem useful + * even if it is safe, so let's block interrupts until done. + */ + HOLD_INTERRUPTS(); + + /* + * Moreover, CurrentMemoryContext might be pointing almost anywhere. We + * don't want to risk leaking data into long-lived contexts, so let's do + * our work here in a private context that we can reset on each use. + */ + if (hpm_context == NULL) /* first time through? */ + hpm_context = AllocSetContextCreate(TopMemoryContext, + "ProcessRepackMessages", + ALLOCSET_DEFAULT_SIZES); + else + MemoryContextReset(hpm_context); + + oldcontext = MemoryContextSwitchTo(hpm_context); + + /* OK to process messages. Reset the flag saying there are more to do. */ + RepackMessagePending = false; + + /* + * Read as many messages as we can from the worker, but stop when no more + * messages can be read from the worker without blocking. + */ + while (true) + { + shm_mq_result res; + Size nbytes; + void *data; + + res = shm_mq_receive(decoding_worker->error_mqh, &nbytes, + &data, true); + if (res == SHM_MQ_WOULD_BLOCK) + break; + else if (res == SHM_MQ_SUCCESS) + { + StringInfoData msg; + + initStringInfo(&msg); + appendBinaryStringInfo(&msg, data, nbytes); + ProcessRepackMessage(&msg); + pfree(msg.data); + } + else + { + /* + * The decoding worker is special in that it exits as soon as it + * has its work done. Thus the DETACHED result code is fine. + */ + Assert(res == SHM_MQ_DETACHED); + + break; + } + } + + MemoryContextSwitchTo(oldcontext); + + /* Might as well clear the context on our way out */ + MemoryContextReset(hpm_context); + + RESUME_INTERRUPTS(); +} + +/* + * Process a single protocol message received from a single parallel worker. + */ +static void +ProcessRepackMessage(StringInfo msg) +{ + char msgtype; + + msgtype = pq_getmsgbyte(msg); + + switch (msgtype) + { + case PqMsg_ErrorResponse: + case PqMsg_NoticeResponse: + { + ErrorData edata; + + /* Parse ErrorResponse or NoticeResponse. */ + pq_parse_errornotice(msg, &edata); + + /* Death of a worker isn't enough justification for suicide. */ + edata.elevel = Min(edata.elevel, ERROR); + + /* + * Add a context line to show that this is a message + * propagated from the worker. Otherwise, it can sometimes be + * confusing to understand what actually happened. + */ + if (edata.context) + edata.context = psprintf("%s\n%s", edata.context, + _("REPACK decoding worker")); + else + edata.context = pstrdup(_("REPACK decoding worker")); + + /* Rethrow error or print notice. */ + ThrowErrorData(&edata); + + break; + } + + default: + { + elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)", + msgtype, msg->len); + } + } +} diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c new file mode 100644 index 00000000000..c85166ba849 --- /dev/null +++ b/src/backend/commands/repack_worker.c @@ -0,0 +1,549 @@ +/*------------------------------------------------------------------------- + * + * repack_worker.c + * Implementation of the background worker for ad-hoc logical decoding + * during REPACK (CONCURRENTLY). + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994-5, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/repack_worker.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/table.h" +#include "access/xlog_internal.h" +#include "access/xlogutils.h" +#include "access/xlogwait.h" +#include "commands/repack.h" +#include "commands/repack_internal.h" +#include "libpq/pqmq.h" +#include "replication/snapbuild.h" +#include "storage/ipc.h" +#include "storage/proc.h" +#include "tcop/tcopprot.h" +#include "utils/memutils.h" + +#define REPL_PLUGIN_NAME "pgoutput_repack" + +static void repack_worker_internal(dsm_segment *seg); +static void RepackWorkerShutdown(int code, Datum arg); +static LogicalDecodingContext *repack_setup_logical_decoding(Oid relid); +static void repack_cleanup_logical_decoding(LogicalDecodingContext *ctx); +static void export_initial_snapshot(Snapshot snapshot, + DecodingWorkerShared *shared); +static bool decode_concurrent_changes(LogicalDecodingContext *ctx, + DecodingWorkerShared *shared); + + +/* Is this process a REPACK worker? */ +static bool am_repack_worker = false; + +/* The WAL segment being decoded. */ +static XLogSegNo repack_current_segment = 0; + +/* + * Keep track of the table we're processing, to skip logical decoding of data + * from other relations. + */ +static RelFileLocator repacked_rel_locator = {.relNumber = InvalidOid}; +static RelFileLocator repacked_rel_toast_locator = {.relNumber = InvalidOid}; + + +/* REPACK decoding worker entry point */ +void +RepackWorkerMain(Datum main_arg) +{ + dsm_segment *seg; + DecodingWorkerShared *shared; + shm_mq *mq; + shm_mq_handle *mqh; + + am_repack_worker = true; + + /* + * Override the default bgworker_die() with die() so we can use + * CHECK_FOR_INTERRUPTS(). + */ + pqsignal(SIGTERM, die); + BackgroundWorkerUnblockSignals(); + + seg = dsm_attach(DatumGetUInt32(main_arg)); + if (seg == NULL) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not map dynamic shared memory segment")); + + shared = (DecodingWorkerShared *) dsm_segment_address(seg); + + /* Arrange to signal the leader if we exit. */ + backend_pid = shared->backend_pid; + backend_proc_number = shared->backend_proc_number; + before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg)); + + /* + * Join locking group - see the comments around the call of + * start_repack_decoding_worker(). + */ + if (!BecomeLockGroupMember(shared->backend_proc, backend_pid)) + /* The leader is not running anymore. */ + return; + + /* + * Setup a queue to send error messages to the backend that launched this + * worker. + */ + mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue); + shm_mq_set_sender(mq, MyProc); + mqh = shm_mq_attach(mq, seg, NULL); + pq_redirect_to_shm_mq(seg, mqh); + pq_set_parallel_leader(shared->backend_pid, + shared->backend_proc_number); + + /* Connect to the database. */ + BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0); + + repack_worker_internal(seg); +} + +static void +repack_worker_internal(dsm_segment *seg) +{ + DecodingWorkerShared *shared; + LogicalDecodingContext *decoding_ctx; + SharedFileSet *sfs; + Snapshot snapshot; + + /* + * Transaction is needed to open relation, and it also provides us with a + * resource owner. + */ + StartTransactionCommand(); + + shared = (DecodingWorkerShared *) dsm_segment_address(seg); + + /* + * Not sure the spinlock is needed here - the backend should not change + * anything in the shared memory until we have serialized the snapshot. + */ + SpinLockAcquire(&shared->mutex); + Assert(!XLogRecPtrIsValid(shared->lsn_upto)); + sfs = &shared->sfs; + SpinLockRelease(&shared->mutex); + + SharedFileSetAttach(sfs, seg); + + /* + * Prepare to capture the concurrent data changes ourselves. + */ + decoding_ctx = repack_setup_logical_decoding(shared->relid); + + /* Announce that we're ready. */ + SpinLockAcquire(&shared->mutex); + shared->initialized = true; + SpinLockRelease(&shared->mutex); + ConditionVariableSignal(&shared->cv); + + /* There doesn't seem to a nice API to set these */ + XactIsoLevel = XACT_REPEATABLE_READ; + XactReadOnly = true; + + /* Build the initial snapshot and export it. */ + snapshot = SnapBuildInitialSnapshot(decoding_ctx->snapshot_builder); + export_initial_snapshot(snapshot, shared); + + /* + * Only historic snapshots should be used now. Do not let us restrict the + * progress of xmin horizon. + */ + InvalidateCatalogSnapshot(); + + for (;;) + { + bool stop = decode_concurrent_changes(decoding_ctx, shared); + + if (stop) + break; + + } + + /* Cleanup. */ + repack_cleanup_logical_decoding(decoding_ctx); + CommitTransactionCommand(); +} + +/* + * See ParallelWorkerShutdown for details. + */ +static void +RepackWorkerShutdown(int code, Datum arg) +{ + SendProcSignal(backend_pid, + PROCSIG_REPACK_MESSAGE, + backend_proc_number); + + dsm_detach((dsm_segment *) DatumGetPointer(arg)); +} + +bool +AmRepackWorker(void) +{ + return am_repack_worker; +} + +/* + * This function is much like pg_create_logical_replication_slot() except that + * the new slot is neither released (if anyone else could read changes from + * our slot, we could miss changes other backends do while we copy the + * existing data into temporary table), nor persisted (it's easier to handle + * crash by restarting all the work from scratch). + */ +static LogicalDecodingContext * +repack_setup_logical_decoding(Oid relid) +{ + Relation rel; + Oid toastrelid; + LogicalDecodingContext *ctx; + NameData slotname; + RepackDecodingState *dstate; + MemoryContext oldcxt; + + /* + * REPACK CONCURRENTLY is not allowed in a transaction block, so this + * should never fire. + */ + Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny())); + + /* + * Make sure we can use logical decoding. + */ + CheckSlotPermissions(); + CheckLogicalDecodingRequirements(); + + /* + * A single backend should not execute multiple REPACK commands at a time, + * so use PID to make the slot unique. + * + * RS_TEMPORARY so that the slot gets cleaned up on ERROR. + */ + snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid); + ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false, + false); + + EnsureLogicalDecodingEnabled(); + + /* + * Neither prepare_write nor do_write callback nor update_progress is + * useful for us. + */ + ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, + NIL, + true, + InvalidXLogRecPtr, + XL_ROUTINE(.page_read = read_local_xlog_page, + .segment_open = wal_segment_open, + .segment_close = wal_segment_close), + NULL, NULL, NULL); + + /* + * We don't have control on setting fast_forward, so at least check it. + */ + Assert(!ctx->fast_forward); + + /* Avoid logical decoding of other relations. */ + rel = table_open(relid, AccessShareLock); + repacked_rel_locator = rel->rd_locator; + toastrelid = rel->rd_rel->reltoastrelid; + if (OidIsValid(toastrelid)) + { + Relation toastrel; + + /* Avoid logical decoding of other TOAST relations. */ + toastrel = table_open(toastrelid, AccessShareLock); + repacked_rel_toast_locator = toastrel->rd_locator; + table_close(toastrel, AccessShareLock); + } + table_close(rel, AccessShareLock); + + DecodingContextFindStartpoint(ctx); + + /* + * decode_concurrent_changes() needs non-blocking callback. + */ + ctx->reader->routine.page_read = read_local_xlog_page_no_wait; + + /* Some WAL records should have been read. */ + Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr); + + /* + * Initialize repack_current_segment so that we can notice WAL segment + * boundaries. + */ + XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment, + wal_segment_size); + + /* Our private state belongs to the decoding context. */ + oldcxt = MemoryContextSwitchTo(ctx->context); + + /* + * read_local_xlog_page_no_wait() needs to be able to indicate the end of + * WAL. + */ + ctx->reader->private_data = palloc0_object(ReadLocalXLogPageNoWaitPrivate); + dstate = palloc0_object(RepackDecodingState); + MemoryContextSwitchTo(oldcxt); + +#ifdef USE_ASSERT_CHECKING + dstate->relid = relid; +#endif + + dstate->change_cxt = AllocSetContextCreate(ctx->context, + "REPACK - change", + ALLOCSET_DEFAULT_SIZES); + + /* The file will be set as soon as we have it opened. */ + dstate->file = NULL; + + /* + * Memory context and resource owner for long-lived resources. + */ + dstate->worker_cxt = CurrentMemoryContext; + dstate->worker_resowner = CurrentResourceOwner; + + ctx->output_writer_private = dstate; + + return ctx; +} + +static void +repack_cleanup_logical_decoding(LogicalDecodingContext *ctx) +{ + RepackDecodingState *dstate; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + if (dstate->slot) + ExecDropSingleTupleTableSlot(dstate->slot); + + FreeDecodingContext(ctx); + ReplicationSlotDropAcquired(); +} + +/* + * Make snapshot available to the backend that launched the decoding worker. + */ +static void +export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared) +{ + char fname[MAXPGPATH]; + BufFile *file; + Size snap_size; + char *snap_space; + + snap_size = EstimateSnapshotSpace(snapshot); + snap_space = (char *) palloc(snap_size); + SerializeSnapshot(snapshot, snap_space); + + DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1); + file = BufFileCreateFileSet(&shared->sfs.fs, fname); + /* To make restoration easier, write the snapshot size first. */ + BufFileWrite(file, &snap_size, sizeof(snap_size)); + BufFileWrite(file, snap_space, snap_size); + pfree(snap_space); + BufFileClose(file); + + /* Increase the counter to tell the backend that the file is available. */ + SpinLockAcquire(&shared->mutex); + shared->last_exported++; + SpinLockRelease(&shared->mutex); + ConditionVariableSignal(&shared->cv); +} + +/* + * Decode logical changes from the WAL sequence and store them to a file. + * + * If true is returned, there is no more work for the worker. + */ +static bool +decode_concurrent_changes(LogicalDecodingContext *ctx, + DecodingWorkerShared *shared) +{ + RepackDecodingState *dstate; + XLogRecPtr lsn_upto; + bool done; + char fname[MAXPGPATH]; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + + /* Open the output file. */ + DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1); + dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname); + + SpinLockAcquire(&shared->mutex); + lsn_upto = shared->lsn_upto; + done = shared->done; + SpinLockRelease(&shared->mutex); + + while (true) + { + XLogRecord *record; + XLogSegNo segno_new; + char *errm = NULL; + XLogRecPtr end_lsn; + + CHECK_FOR_INTERRUPTS(); + + record = XLogReadRecord(ctx->reader, &errm); + if (record) + { + LogicalDecodingProcessRecord(ctx, ctx->reader); + + /* + * If WAL segment boundary has been crossed, inform the decoding + * system that the catalog_xmin can advance. + * + * TODO Does it make sense to confirm more often? Segment size + * seems appropriate for restart_lsn (because less than a segment + * cannot be recycled anyway), however more frequent checks might + * be beneficial for catalog_xmin. + */ + end_lsn = ctx->reader->EndRecPtr; + XLByteToSeg(end_lsn, segno_new, wal_segment_size); + if (segno_new != repack_current_segment) + { + LogicalConfirmReceivedLocation(end_lsn); + elog(DEBUG1, "REPACK: confirmed receive location %X/%X", + (uint32) (end_lsn >> 32), (uint32) end_lsn); + repack_current_segment = segno_new; + } + } + else + { + ReadLocalXLogPageNoWaitPrivate *priv; + + if (errm) + ereport(ERROR, + errmsg("%s", errm)); + + /* + * In the decoding loop we do not want to get blocked when there + * is no more WAL available, otherwise the loop would become + * uninterruptible. + */ + priv = (ReadLocalXLogPageNoWaitPrivate *) ctx->reader->private_data; + if (priv->end_of_wal) + /* Do not miss the end of WAL condition next time. */ + priv->end_of_wal = false; + else + ereport(ERROR, + errmsg("could not read WAL record")); + } + + /* + * Whether we could read new record or not, keep checking if + * 'lsn_upto' was specified. + */ + if (!XLogRecPtrIsValid(lsn_upto)) + { + SpinLockAcquire(&shared->mutex); + lsn_upto = shared->lsn_upto; + /* 'done' should be set at the same time as 'lsn_upto' */ + done = shared->done; + SpinLockRelease(&shared->mutex); + } + if (XLogRecPtrIsValid(lsn_upto) && + ctx->reader->EndRecPtr >= lsn_upto) + break; + + if (record == NULL) + { + int64 timeout = 0; + WaitLSNResult res; + + /* + * Before we retry reading, wait until new WAL is flushed. + * + * There is a race condition such that the backend executing + * REPACK determines 'lsn_upto', but before it sets the shared + * variable, we reach the end of WAL. In that case we'd need to + * wait until the next WAL flush (unrelated to REPACK). Although + * that should not be a problem in a busy system, it might be + * noticeable in other cases, including regression tests (which + * are not necessarily executed in parallel). Therefore it makes + * sense to use timeout. + * + * If lsn_upto is valid, WAL records having LSN lower than that + * should already have been flushed to disk. + */ + if (!XLogRecPtrIsValid(lsn_upto)) + timeout = 100L; + res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH, + ctx->reader->EndRecPtr + 1, + timeout); + if (res != WAIT_LSN_RESULT_SUCCESS && + res != WAIT_LSN_RESULT_TIMEOUT) + ereport(ERROR, + errmsg("waiting for WAL failed")); + } + } + + /* + * Close the file so we can make it available to the backend. + */ + BufFileClose(dstate->file); + dstate->file = NULL; + SpinLockAcquire(&shared->mutex); + shared->lsn_upto = InvalidXLogRecPtr; + shared->last_exported++; + SpinLockRelease(&shared->mutex); + ConditionVariableSignal(&shared->cv); + + return done; +} + +/* + * Does the WAL record contain a data change that this backend does not need + * to decode on behalf of REPACK (CONCURRENTLY)? + */ +bool +change_useless_for_repack(XLogRecordBuffer *buf) +{ + XLogReaderState *r = buf->record; + RelFileLocator locator; + + /* TOAST locator should not be set unless the main is. */ + Assert(!OidIsValid(repacked_rel_toast_locator.relNumber) || + OidIsValid(repacked_rel_locator.relNumber)); + + /* + * Backends not involved in REPACK (CONCURRENTLY) should not do the + * filtering. + */ + if (!OidIsValid(repacked_rel_locator.relNumber)) + return false; + + /* + * If the record does not contain the block 0, it's probably not INSERT / + * UPDATE / DELETE. In any case, we do not have enough information to + * filter the change out. + */ + if (!XLogRecGetBlockTagExtended(r, 0, &locator, NULL, NULL, NULL)) + return false; + + /* + * Decode the change if it belongs to the table we are repacking, or if it + * belongs to its TOAST relation. + */ + if (RelFileLocatorEquals(locator, repacked_rel_locator)) + return false; + if (OidIsValid(repacked_rel_toast_locator.relNumber) && + RelFileLocatorEquals(locator, repacked_rel_toast_locator)) + return false; + + /* Filter out changes of other tables. */ + return true; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index e2882a50b3b..eec09ba1ded 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -6058,6 +6058,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode, finish_heap_swap(tab->relid, OIDNewHeap, false, false, true, !OidIsValid(tab->newTableSpace), + true, /* reindex */ RecentXmin, ReadNextMultiXactId(), persistence); diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index b179b62b5c8..149d15f8f91 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -127,7 +127,7 @@ static void vac_truncate_clog(TransactionId frozenXID, TransactionId lastSaneFrozenXid, MultiXactId lastSaneMinMulti); static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params, - BufferAccessStrategy bstrategy); + BufferAccessStrategy bstrategy, bool isTopLevel); static double compute_parallel_delay(void); static VacOptValue get_vacoptval_from_boolean(DefElem *def); static bool vac_tid_reaped(ItemPointer itemptr, void *state); @@ -630,7 +630,8 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate if (params->options & VACOPT_VACUUM) { - if (!vacuum_rel(vrel->oid, vrel->relation, *params, bstrategy)) + if (!vacuum_rel(vrel->oid, vrel->relation, *params, bstrategy, + isTopLevel)) continue; } @@ -2004,7 +2005,7 @@ vac_truncate_clog(TransactionId frozenXID, */ static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params, - BufferAccessStrategy bstrategy) + BufferAccessStrategy bstrategy, bool isTopLevel) { LOCKMODE lmode; Relation rel; @@ -2295,7 +2296,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params, /* VACUUM FULL is a variant of REPACK; see repack.c */ cluster_rel(REPACK_COMMAND_VACUUMFULL, rel, InvalidOid, - &cluster_params); + &cluster_params, isTopLevel); /* cluster_rel closes the relation, but keeps lock */ rel = NULL; @@ -2338,7 +2339,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params, toast_vacuum_params.options |= VACOPT_PROCESS_MAIN; toast_vacuum_params.toast_parent = relid; - vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy); + vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy, + isTopLevel); } /* diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index dfd7b33aa9b..a09b96a0868 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -2575,7 +2575,7 @@ lreplace: 0, estate->es_snapshot, estate->es_crosscheck_snapshot, - true /* wait for commit */ , + true, &context->tmfd, &updateCxt->lockmode, &updateCxt->updateIndexes); diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c index 22e5164adbf..21ce180c78d 100644 --- a/src/backend/libpq/pqmq.c +++ b/src/backend/libpq/pqmq.c @@ -14,6 +14,7 @@ #include "postgres.h" #include "access/parallel.h" +#include "commands/repack.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" #include "libpq/pqmq.h" @@ -177,6 +178,10 @@ mq_putmessage(char msgtype, const char *s, size_t len) SendProcSignal(pq_mq_parallel_leader_pid, PROCSIG_PARALLEL_APPLY_MESSAGE, pq_mq_parallel_leader_proc_number); + else if (AmRepackWorker()) + SendProcSignal(pq_mq_parallel_leader_pid, + PROCSIG_REPACK_MESSAGE, + pq_mq_parallel_leader_proc_number); else { Assert(IsParallelWorker()); diff --git a/src/backend/meson.build b/src/backend/meson.build index 4f5292d8f88..5e3cfe2d6f8 100644 --- a/src/backend/meson.build +++ b/src/backend/meson.build @@ -219,5 +219,6 @@ pg_test_mod_args = pg_mod_args + { subdir('jit/llvm') subdir('replication/libpqwalreceiver') subdir('replication/pgoutput') +subdir('replication/pgoutput_repack') subdir('snowball') subdir('utils/mb/conversion_procs') diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c index f2a62489d9c..bc477361e7a 100644 --- a/src/backend/postmaster/bgworker.c +++ b/src/backend/postmaster/bgworker.c @@ -13,6 +13,7 @@ #include "postgres.h" #include "access/parallel.h" +#include "commands/repack.h" #include "libpq/pqsignal.h" #include "miscadmin.h" #include "pgstat.h" @@ -136,6 +137,10 @@ static const struct .fn_name = "ParallelWorkerMain", .fn_addr = ParallelWorkerMain }, + { + .fn_name = "RepackWorkerMain", + .fn_addr = RepackWorkerMain + }, { .fn_name = "SequenceSyncWorkerMain", .fn_addr = SequenceSyncWorkerMain @@ -144,7 +149,6 @@ static const struct .fn_name = "TableSyncWorkerMain", .fn_addr = TableSyncWorkerMain }, - }; /* Private functions. */ diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c index 3c027bcb2f7..29938b26d53 100644 --- a/src/backend/replication/logical/decode.c +++ b/src/backend/replication/logical/decode.c @@ -33,6 +33,7 @@ #include "access/xlogreader.h" #include "access/xlogrecord.h" #include "catalog/pg_control.h" +#include "commands/repack.h" #include "replication/decode.h" #include "replication/logical.h" #include "replication/message.h" @@ -420,7 +421,8 @@ heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) { case XLOG_HEAP2_MULTI_INSERT: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeMultiInsert(ctx, buf); break; case XLOG_HEAP2_NEW_CID: @@ -482,7 +484,8 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) { case XLOG_HEAP_INSERT: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeInsert(ctx, buf); break; @@ -494,19 +497,22 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) case XLOG_HEAP_HOT_UPDATE: case XLOG_HEAP_UPDATE: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeUpdate(ctx, buf); break; case XLOG_HEAP_DELETE: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeDelete(ctx, buf); break; case XLOG_HEAP_TRUNCATE: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeTruncate(ctx, buf); break; @@ -522,7 +528,8 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) case XLOG_HEAP_CONFIRM: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeSpecConfirm(ctx, buf); break; @@ -1019,6 +1026,15 @@ DecodeDelete(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) xlrec = (xl_heap_delete *) XLogRecGetData(r); + /* + * Ignore changes which are considered useless for logical decoding. + * Currently such changes are created by REPACK (CONCURRENTLY) when + * replays DELETE commands on the new table (which is not yet visible to + * other transactions). + */ + if (xlrec->flags & XLH_DELETE_NO_LOGICAL) + return; + /* only interested in our database */ XLogRecGetBlockTag(r, 0, &target_locator, NULL, NULL); if (target_locator.dbOid != ctx->slot->data.database) diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c index f20d0c542f3..f20a0fe70ad 100644 --- a/src/backend/replication/logical/logical.c +++ b/src/backend/replication/logical/logical.c @@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options, ctx->slot = slot; - ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx); + /* + * TODO A separate patch for PG core, unless there's really a reason to + * pass ctx for private_data (May extensions expect ctx?). + */ + ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL); if (!ctx->reader) ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), diff --git a/src/backend/replication/pgoutput_repack/Makefile b/src/backend/replication/pgoutput_repack/Makefile new file mode 100644 index 00000000000..4efeb713b70 --- /dev/null +++ b/src/backend/replication/pgoutput_repack/Makefile @@ -0,0 +1,32 @@ +#------------------------------------------------------------------------- +# +# Makefile-- +# Makefile for src/backend/replication/pgoutput_repack +# +# IDENTIFICATION +# src/backend/replication/pgoutput_repack +# +#------------------------------------------------------------------------- + +subdir = src/backend/replication/pgoutput_repack +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global + +OBJS = \ + $(WIN32RES) \ + pgoutput_repack.o +PGFILEDESC = "pgoutput_repack - logical replication output plugin for REPACK command" +NAME = pgoutput_repack + +all: all-shared-lib + +include $(top_srcdir)/src/Makefile.shlib + +install: all installdirs install-lib + +installdirs: installdirs-lib + +uninstall: uninstall-lib + +clean distclean: clean-lib + rm -f $(OBJS) diff --git a/src/backend/replication/pgoutput_repack/meson.build b/src/backend/replication/pgoutput_repack/meson.build new file mode 100644 index 00000000000..6a88c0fb08d --- /dev/null +++ b/src/backend/replication/pgoutput_repack/meson.build @@ -0,0 +1,18 @@ +# Copyright (c) 2022-2026, PostgreSQL Global Development Group + +pgoutput_repack_sources = files( + 'pgoutput_repack.c', +) + +if host_system == 'windows' + pgoutput_repack_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'pgoutput_repack', + '--FILEDESC', 'pgoutput_repack - logical replication output plugin for REPACK command',]) +endif + +pgoutput_repack = shared_module('pgoutput_repack', + pgoutput_repack_sources, + kwargs: pg_mod_args, +) + +backend_targets += pgoutput_repack diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c new file mode 100644 index 00000000000..221260ca0a8 --- /dev/null +++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c @@ -0,0 +1,290 @@ +/*------------------------------------------------------------------------- + * + * pgoutput_repack.c + * Logical Replication output plugin for REPACK command + * + * Copyright (c) 2012-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/replication/pgoutput_repack/pgoutput_repack.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/detoast.h" +#include "commands/repack_internal.h" +#include "replication/snapbuild.h" +#include "utils/memutils.h" + +PG_MODULE_MAGIC; + +static void repack_startup(LogicalDecodingContext *ctx, + OutputPluginOptions *opt, bool is_init); +static void repack_shutdown(LogicalDecodingContext *ctx); +static void repack_begin_txn(LogicalDecodingContext *ctx, + ReorderBufferTXN *txn); +static void repack_commit_txn(LogicalDecodingContext *ctx, + ReorderBufferTXN *txn, XLogRecPtr commit_lsn); +static void repack_process_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, + Relation rel, ReorderBufferChange *change); +static void repack_store_change(LogicalDecodingContext *ctx, Relation relation, + ConcurrentChangeKind kind, HeapTuple tuple); + +void +_PG_output_plugin_init(OutputPluginCallbacks *cb) +{ + cb->startup_cb = repack_startup; + cb->begin_cb = repack_begin_txn; + cb->change_cb = repack_process_change; + cb->commit_cb = repack_commit_txn; + cb->shutdown_cb = repack_shutdown; +} + + +/* initialize this plugin */ +static void +repack_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt, + bool is_init) +{ + ctx->output_plugin_private = NULL; + + /* Probably unnecessary, as we don't use the SQL interface ... */ + opt->output_type = OUTPUT_PLUGIN_BINARY_OUTPUT; + + if (ctx->output_plugin_options != NIL) + { + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("this plugin does not expect any options")); + } +} + +static void +repack_shutdown(LogicalDecodingContext *ctx) +{ +} + +/* + * As we don't release the slot during processing of particular table, there's + * no room for SQL interface, even for debugging purposes. Therefore we need + * neither OutputPluginPrepareWrite() nor OutputPluginWrite() in the plugin + * callbacks. (Although we might want to write custom callbacks, this API + * seems to be unnecessarily generic for our purposes.) + */ + +/* BEGIN callback */ +static void +repack_begin_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn) +{ +} + +/* COMMIT callback */ +static void +repack_commit_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, + XLogRecPtr commit_lsn) +{ +} + +/* + * Callback for individual changed tuples + */ +static void +repack_process_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, + Relation relation, ReorderBufferChange *change) +{ + RepackDecodingState *private PG_USED_FOR_ASSERTS_ONLY = + (RepackDecodingState *) ctx->output_writer_private; + + /* Changes of other relation should not have been decoded. */ + Assert(RelationGetRelid(relation) == private->relid); + + /* Decode entry depending on its type */ + switch (change->action) + { + case REORDER_BUFFER_CHANGE_INSERT: + { + HeapTuple newtuple; + + newtuple = change->data.tp.newtuple; + + /* + * Identity checks in the main function should have made this + * impossible. + */ + if (newtuple == NULL) + elog(ERROR, "incomplete insert info"); + + repack_store_change(ctx, relation, CHANGE_INSERT, newtuple); + } + break; + case REORDER_BUFFER_CHANGE_UPDATE: + { + HeapTuple oldtuple, + newtuple; + + oldtuple = change->data.tp.oldtuple; + newtuple = change->data.tp.newtuple; + + if (newtuple == NULL) + elog(ERROR, "incomplete update info"); + + if (oldtuple != NULL) + repack_store_change(ctx, relation, CHANGE_UPDATE_OLD, oldtuple); + + repack_store_change(ctx, relation, CHANGE_UPDATE_NEW, newtuple); + } + break; + case REORDER_BUFFER_CHANGE_DELETE: + { + HeapTuple oldtuple; + + oldtuple = change->data.tp.oldtuple; + + if (oldtuple == NULL) + elog(ERROR, "incomplete delete info"); + + repack_store_change(ctx, relation, CHANGE_DELETE, oldtuple); + } + break; + default: + + /* + * Should not come here. This includes TRUNCATE of the table being + * processed. heap_decode() cannot check the file locator easily, + * but we assume that TRUNCATE uses AccessExclusiveLock on the + * table so it should not occur during REPACK (CONCURRENTLY). + */ + Assert(false); + break; + } +} + +/* + * Write the given tuple, with the given change kind, to the repack spill + * file. Later, the repack decoding worker can read these and replay + * the operations on the new copy of the table. + * + * For each change affecting the table being repacked, we store enough + * information about each tuple in it, so that it can be replayed in the + * new copy of the table. + * + * XXX for DELETE and the UPDATE OLD tuples, we could store just the + * replication identity instead of the full tuple. + */ +static void +repack_store_change(LogicalDecodingContext *ctx, Relation relation, + ConcurrentChangeKind kind, HeapTuple tuple) +{ + RepackDecodingState *dstate; + MemoryContext oldcxt; + BufFile *file; + List *attrs_ext = NIL; + int natt_ext; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + file = dstate->file; + + /* Store the change kind. */ + BufFileWrite(file, &kind, 1); + + /* Use a frequently-reset context to avoid dealing with leaks manually */ + oldcxt = MemoryContextSwitchTo(dstate->change_cxt); + + /* + * If the tuple contains "external indirect" attributes, we need to write + * the contents to the file because we have no control over that memory. + */ + if (HeapTupleHasExternal(tuple)) + { + TupleDesc desc = RelationGetDescr(relation); + TupleTableSlot *slot; + + /* Initialize the slot, if not done already */ + if (dstate->slot == NULL) + { + ResourceOwner saveResourceOwner; + + MemoryContextSwitchTo(dstate->worker_cxt); + saveResourceOwner = CurrentResourceOwner; + CurrentResourceOwner = dstate->worker_resowner; + dstate->slot = MakeSingleTupleTableSlot(desc, &TTSOpsHeapTuple); + MemoryContextSwitchTo(dstate->change_cxt); + CurrentResourceOwner = saveResourceOwner; + } + + slot = dstate->slot; + ExecStoreHeapTuple(tuple, slot, false); + + /* + * Loop over all attributes, and find out which ones we need to spill + * separately, to wit: each one that's a non-null varlena and stored + * out of line. + */ + for (int i = 0; i < desc->natts; i++) + { + CompactAttribute *attr = TupleDescCompactAttr(desc, i); + varlena *varlen; + + if (attr->attisdropped || attr->attlen != -1 || + slot_attisnull(slot, i + 1)) + continue; + + slot_getsomeattrs(slot, i + 1); + + /* + * This is a non-null varlena datum, but we only care if it's + * out-of-line + */ + varlen = (varlena *) DatumGetPointer(slot->tts_values[i]); + if (!VARATT_IS_EXTERNAL(varlen)) + continue; + + /* + * We spill any indirect-external attributes separately from the + * heap tuple. Anything else is written as is. + */ + if (VARATT_IS_EXTERNAL_INDIRECT(varlen)) + attrs_ext = lappend(attrs_ext, varlen); + else + { + /* + * Logical decoding should not produce "external expanded" + * attributes (those actually should never appear on disk), so + * only TOASTed attribute can be seen here. + * + * We get here if the table has external values but only + * in-line values are being updated now. + */ + Assert(VARATT_IS_EXTERNAL_ONDISK(varlen)); + } + } + + ExecClearTuple(slot); + } + + /* + * First, write the original heap tuple, prefixed by its length. Note + * that the external-toast tag for each toasted attribute will be present + * in what we write, so that we know where to restore each one later. + */ + BufFileWrite(file, &tuple->t_len, sizeof(tuple->t_len)); + BufFileWrite(file, tuple->t_data, tuple->t_len); + + /* Then, write the number of external attributes we found. */ + natt_ext = list_length(attrs_ext); + BufFileWrite(file, &natt_ext, sizeof(natt_ext)); + + /* Finally, the attributes themselves, if any */ + foreach_ptr(varlena, attr_val, attrs_ext) + { + attr_val = detoast_external_attr(attr_val); + BufFileWrite(file, attr_val, VARSIZE_ANY(attr_val)); + /* These attributes could be large, so free them right away */ + pfree(attr_val); + } + + /* Cleanup. */ + MemoryContextSwitchTo(oldcxt); + MemoryContextReset(dstate->change_cxt); +} diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 7cef6e43661..f02b9eb8312 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -701,6 +701,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); + if (CheckProcSignal(PROCSIG_REPACK_MESSAGE)) + HandleRepackMessageInterrupt(); + if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT)) HandleRecoveryConflictInterrupt(); diff --git a/src/backend/storage/lmgr/generate-lwlocknames.pl b/src/backend/storage/lmgr/generate-lwlocknames.pl index b49007167b0..2e7f1054e62 100644 --- a/src/backend/storage/lmgr/generate-lwlocknames.pl +++ b/src/backend/storage/lmgr/generate-lwlocknames.pl @@ -162,7 +162,7 @@ while (<$lwlocklist>) die "$wait_event_lwlocks[$lwlock_count] defined in wait_event_names.txt but " - . " missing from lwlocklist.h" + . "missing from lwlocklist.h" if $lwlock_count < scalar @wait_event_lwlocks; die diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 9fbaa5c00f0..47a2960a197 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3577,6 +3577,9 @@ ProcessInterrupts(void) if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); + + if (RepackMessagePending) + ProcessRepackMessages(); } /* diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 6be80d2daad..e2f21349997 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace." RECOVERY_END_COMMAND "Waiting for to complete." RECOVERY_PAUSE "Waiting for recovery to be resumed." +REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file." REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped." REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped." RESTORE_COMMAND "Waiting for to complete." diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 53bf1e21721..1a22f162ce6 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -5231,8 +5231,8 @@ match_previous_words(int pattern_id, * one word, so the above test is correct. */ if (ends_with(prev_wd, '(') || ends_with(prev_wd, ',')) - COMPLETE_WITH("ANALYZE", "VERBOSE"); - else if (TailMatches("ANALYZE", "VERBOSE")) + COMPLETE_WITH("ANALYZE", "CONCURRENTLY", "VERBOSE"); + else if (TailMatches("ANALYZE", "CONCURRENTLY", "VERBOSE")) COMPLETE_WITH("ON", "OFF"); } diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h index 516806fcca2..fdca7d821c8 100644 --- a/src/include/access/heapam_xlog.h +++ b/src/include/access/heapam_xlog.h @@ -104,6 +104,8 @@ #define XLH_DELETE_CONTAINS_OLD_KEY (1<<2) #define XLH_DELETE_IS_SUPER (1<<3) #define XLH_DELETE_IS_PARTITION_MOVE (1<<4) +/* See heap_delete() */ +#define XLH_DELETE_NO_LOGICAL (1<<5) /* convenience macro for checking whether any form of old tuple was logged */ #define XLH_DELETE_CONTAINS_OLD \ diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h index 4647785fd35..e95073035af 100644 --- a/src/include/access/tableam.h +++ b/src/include/access/tableam.h @@ -284,9 +284,10 @@ typedef struct TM_IndexDeleteOp /* "options" flag bits for table_tuple_delete */ #define TABLE_DELETE_CHANGING_PARTITION (1 << 0) +#define TABLE_DELETE_NO_LOGICAL (1 << 1) /* "options" flag bits for table_tuple_update */ -/* XXX none at present */ +#define TABLE_UPDATE_NO_LOGICAL (1 << 0) /* flag bits for table_tuple_lock */ /* Follow tuples whose update is in progress if lock modes don't conflict */ @@ -662,6 +663,7 @@ typedef struct TableAmRoutine Relation OldIndex, bool use_sort, TransactionId OldestXmin, + Snapshot snapshot, TransactionId *xid_cutoff, MultiXactId *multi_cutoff, double *num_tuples, @@ -1527,6 +1529,7 @@ table_multi_insert(Relation rel, TupleTableSlot **slots, int nslots, * TABLE_DELETE_CHANGING_PARTITION: the tuple is being moved to another * partition table due to an update of the partition key. * crosscheck - if not InvalidSnapshot, also check tuple against this + * XXX document options * wait - true if should wait for any conflicting update to commit/abort * * Output parameters: @@ -1563,7 +1566,12 @@ table_tuple_delete(Relation rel, ItemPointer tid, CommandId cid, * cmax/cmin if successful) * options - bitmask of options. No values are currently recognized. * crosscheck - if not InvalidSnapshot, also check old tuple against this - * wait - true if should wait for any conflicting update to commit/abort + * options - These allow the caller to specify options that may change the + * behavior of the AM. The AM will ignore options that it does not support. + * TABLE_UPDATE_WAIT -- set if should wait for any conflicting update to + * commit/abort + * TABLE_UPDATE_NO_LOGICAL -- force-disables the emitting of logical + * decoding information for the tuple. * * Output parameters: * slot - newly constructed tuple data to store @@ -1725,6 +1733,8 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator) * not needed for the relation's AM * - *xid_cutoff - ditto * - *multi_cutoff - ditto + * - snapshot - if != NULL, ignore data changes done by transactions that this + * (MVCC) snapshot considers still in-progress or in the future. * * Output parameters: * - *xid_cutoff - rel's new relfrozenxid value, may be invalid @@ -1737,6 +1747,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable, Relation OldIndex, bool use_sort, TransactionId OldestXmin, + Snapshot snapshot, TransactionId *xid_cutoff, MultiXactId *multi_cutoff, double *num_tuples, @@ -1745,6 +1756,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable, { OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex, use_sort, OldestXmin, + snapshot, xid_cutoff, multi_cutoff, num_tuples, tups_vacuumed, tups_recently_dead); diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h index 9c40772706c..b5b01c1bb6d 100644 --- a/src/include/commands/progress.h +++ b/src/include/commands/progress.h @@ -86,10 +86,12 @@ #define PROGRESS_REPACK_PHASE 1 #define PROGRESS_REPACK_INDEX_RELID 2 #define PROGRESS_REPACK_HEAP_TUPLES_SCANNED 3 -#define PROGRESS_REPACK_HEAP_TUPLES_WRITTEN 4 -#define PROGRESS_REPACK_TOTAL_HEAP_BLKS 5 -#define PROGRESS_REPACK_HEAP_BLKS_SCANNED 6 -#define PROGRESS_REPACK_INDEX_REBUILD_COUNT 7 +#define PROGRESS_REPACK_HEAP_TUPLES_INSERTED 4 +#define PROGRESS_REPACK_HEAP_TUPLES_UPDATED 5 +#define PROGRESS_REPACK_HEAP_TUPLES_DELETED 6 +#define PROGRESS_REPACK_TOTAL_HEAP_BLKS 7 +#define PROGRESS_REPACK_HEAP_BLKS_SCANNED 8 +#define PROGRESS_REPACK_INDEX_REBUILD_COUNT 9 /* * Phases of repack (as advertised via PROGRESS_REPACK_PHASE). @@ -98,9 +100,10 @@ #define PROGRESS_REPACK_PHASE_INDEX_SCAN_HEAP 2 #define PROGRESS_REPACK_PHASE_SORT_TUPLES 3 #define PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP 4 -#define PROGRESS_REPACK_PHASE_SWAP_REL_FILES 5 -#define PROGRESS_REPACK_PHASE_REBUILD_INDEX 6 -#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP 7 +#define PROGRESS_REPACK_PHASE_CATCH_UP 5 +#define PROGRESS_REPACK_PHASE_SWAP_REL_FILES 6 +#define PROGRESS_REPACK_PHASE_REBUILD_INDEX 7 +#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP 8 /* Progress parameters for CREATE INDEX */ /* 3, 4 and 5 reserved for "waitfor" metrics */ diff --git a/src/include/commands/repack.h b/src/include/commands/repack.h index 85061158b0c..fd16e74b179 100644 --- a/src/include/commands/repack.h +++ b/src/include/commands/repack.h @@ -13,6 +13,8 @@ #ifndef REPACK_H #define REPACK_H +#include + #include "nodes/parsenodes.h" #include "parser/parse_node.h" #include "storage/lockdefs.h" @@ -25,6 +27,7 @@ #define CLUOPT_RECHECK_ISCLUSTERED 0x04 /* recheck relation state for * indisclustered */ #define CLUOPT_ANALYZE 0x08 /* do an ANALYZE */ +#define CLUOPT_CONCURRENT 0x10 /* allow concurrent data changes */ /* options for CLUSTER */ typedef struct ClusterParams @@ -32,11 +35,13 @@ typedef struct ClusterParams uint32 options; /* bitmask of CLUOPT_* */ } ClusterParams; +extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending; + extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel); extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid, - ClusterParams *params); + ClusterParams *params, bool isTopLevel); extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode); extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal); @@ -48,8 +53,16 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, bool swap_toast_by_content, bool check_constraints, bool is_internal, + bool reindex, TransactionId frozenXid, MultiXactId cutoffMulti, char newrelpersistence); +extern void HandleRepackMessageInterrupt(void); +extern void ProcessRepackMessages(void); + +/* in repack_worker.c */ +extern void RepackWorkerMain(Datum main_arg); +extern bool AmRepackWorker(void); + #endif /* REPACK_H */ diff --git a/src/include/commands/repack_internal.h b/src/include/commands/repack_internal.h new file mode 100644 index 00000000000..cf495afd6a4 --- /dev/null +++ b/src/include/commands/repack_internal.h @@ -0,0 +1,128 @@ +/*------------------------------------------------------------------------- + * + * repack_internal.h + * header for REPACK internals + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994-5, Regents of the University of California + * + * src/include/commands/repack_internal.h + * + *------------------------------------------------------------------------- + */ +#ifndef REPACK_INTERNAL_H +#define REPACK_INTERNAL_H + +#include "nodes/execnodes.h" +#include "replication/decode.h" +#include "postmaster/bgworker.h" +#include "replication/logical.h" +#include "storage/buffile.h" +#include "storage/sharedfileset.h" +#include "storage/shm_mq.h" +#include "utils/resowner.h" + +/* + * Stored as a single byte in the output file. + */ +typedef char ConcurrentChangeKind; + +#define CHANGE_INSERT 'i' +#define CHANGE_UPDATE_OLD 'u' +#define CHANGE_UPDATE_NEW 'U' +#define CHANGE_DELETE 'd' + +extern pid_t backend_pid; +extern ProcNumber backend_proc_number; + +/* + * Logical decoding state. + * + * The output plugin uses it to store the data changes that it decodes from + * WAL while the table contents is being copied to a new storage. + */ +typedef struct RepackDecodingState +{ +#ifdef USE_ASSERT_CHECKING + /* The relation whose changes we're decoding. */ + Oid relid; +#endif + + /* Per-change memory context. */ + MemoryContext change_cxt; + + /* A tuple slot used to pass tuples back and forth */ + TupleTableSlot *slot; + + /* + * Memory context and resource owner of the decoding worker's transaction. + */ + MemoryContext worker_cxt; + ResourceOwner worker_resowner; + + /* The current output file. */ + BufFile *file; +} RepackDecodingState; + +/* + * Layout of shared memory used for communication between backend and the + * worker that performs logical decoding of data changes + */ +typedef struct DecodingWorkerShared +{ + /* Is the decoding initialized? */ + bool initialized; + + /* + * Once the worker has reached this LSN, it should close the current + * output file and either create a new one or exit, according to the field + * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all + * the WAL available and keep checking this field. It is ok if the worker + * had already decoded records whose LSN is >= lsn_upto before this field + * has been set. + */ + XLogRecPtr lsn_upto; + + /* Exit after closing the current file? */ + bool done; + + /* The output is stored here. */ + SharedFileSet sfs; + + /* Number of the last file exported by the worker. */ + int last_exported; + + /* Synchronize access to the fields above. */ + slock_t mutex; + + /* Database to connect to. */ + Oid dbid; + + /* Role to connect as. */ + Oid roleid; + + /* Decode data changes of this relation. */ + Oid relid; + + /* The backend uses this to wait for the worker. */ + ConditionVariable cv; + + /* Info to signal the backend. */ + PGPROC *backend_proc; + pid_t backend_pid; + ProcNumber backend_proc_number; + + /* + * Memory the queue is located in. + * + * For considerations on the value see the comments of + * PARALLEL_ERROR_QUEUE_SIZE. + */ +#define REPACK_ERROR_QUEUE_SIZE 16384 + char error_queue[FLEXIBLE_ARRAY_MEMBER]; +} DecodingWorkerShared; + +extern void DecodingWorkerFileName(char *fname, Oid relid, uint32 seq); + + +#endif /* REPACK_INTERNAL_H */ diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h index 49f00fc48b8..188f1a875f7 100644 --- a/src/include/replication/decode.h +++ b/src/include/replication/decode.h @@ -31,4 +31,8 @@ extern void logicalmsg_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf extern void LogicalDecodingProcessRecord(LogicalDecodingContext *ctx, XLogReaderState *record); +/* in commands/repack_worker.c */ +extern bool change_useless_for_repack(XLogRecordBuffer *buf); + + #endif diff --git a/src/include/storage/lockdefs.h b/src/include/storage/lockdefs.h index b73bb5618e6..3785b009808 100644 --- a/src/include/storage/lockdefs.h +++ b/src/include/storage/lockdefs.h @@ -36,8 +36,8 @@ typedef int LOCKMODE; #define AccessShareLock 1 /* SELECT */ #define RowShareLock 2 /* SELECT FOR UPDATE/FOR SHARE */ #define RowExclusiveLock 3 /* INSERT, UPDATE, DELETE */ -#define ShareUpdateExclusiveLock 4 /* VACUUM (non-FULL), ANALYZE, CREATE - * INDEX CONCURRENTLY */ +#define ShareUpdateExclusiveLock 4 /* VACUUM (non-exclusive), ANALYZE, CREATE + * INDEX CONCURRENTLY, REPACK CONCURRENTLY */ #define ShareLock 5 /* CREATE INDEX (WITHOUT CONCURRENTLY) */ #define ShareRowExclusiveLock 6 /* like EXCLUSIVE MODE, but allows ROW * SHARE */ diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 348fba53a93..a944ee0d211 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -36,6 +36,7 @@ typedef enum PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ + PROCSIG_REPACK_MESSAGE, /* Message from repack worker */ PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check * PGPROC->pendingRecoveryConflicts for the * reason */ diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index a41d781f8c9..2cd7d87c533 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -14,6 +14,8 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress ISOLATION = basic \ inplace \ + repack \ + repack_toast \ syscache-update-pruned \ heap_lock_update diff --git a/src/test/modules/injection_points/expected/repack.out b/src/test/modules/injection_points/expected/repack.out new file mode 100644 index 00000000000..b575e9052ee --- /dev/null +++ b/src/test/modules/injection_points/expected/repack.out @@ -0,0 +1,113 @@ +Parsed test spec with 2 sessions + +starting permutation: wait_before_lock change_existing change_new change_subxact1 change_subxact2 check2 wakeup_before_lock check1 +injection_points_attach +----------------------- + +(1 row) + +step wait_before_lock: + REPACK (CONCURRENTLY) repack_test USING INDEX repack_test_pkey; + +step change_existing: + UPDATE repack_test SET i=10 where i=1; + UPDATE repack_test SET j=20 where i=2; + UPDATE repack_test SET i=30 where i=3; + UPDATE repack_test SET i=40 where i=30; + DELETE FROM repack_test WHERE i=4; + +step change_new: + INSERT INTO repack_test(i, j) VALUES (5, 5), (6, 6), (7, 7), (8, 8); + UPDATE repack_test SET i=50 where i=5; + UPDATE repack_test SET j=60 where i=6; + DELETE FROM repack_test WHERE i=7; + +step change_subxact1: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (100, 100); + SAVEPOINT s1; + UPDATE repack_test SET i=101 where i=100; + SAVEPOINT s2; + UPDATE repack_test SET i=102 where i=101; + COMMIT; + +step change_subxact2: + BEGIN; + SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 110); + ROLLBACK TO SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 111); + COMMIT; + +step check2: + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; + + i| j +---+--- + 2| 20 + 6| 60 + 8| 8 + 10| 1 + 40| 3 + 50| 5 +102|100 +110|111 +(8 rows) + +step wakeup_before_lock: + SELECT injection_points_wakeup('repack-concurrently-before-lock'); + +injection_points_wakeup +----------------------- + +(1 row) + +step wait_before_lock: <... completed> +step check1: + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; + +count +----- + 2 +(1 row) + + i| j +---+--- + 2| 20 + 6| 60 + 8| 8 + 10| 1 + 40| 3 + 50| 5 +102|100 +110|111 +(8 rows) + +count +----- + 0 +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/expected/repack_toast.out b/src/test/modules/injection_points/expected/repack_toast.out new file mode 100644 index 00000000000..b56dde134f8 --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_toast.out @@ -0,0 +1,65 @@ +Parsed test spec with 2 sessions + +starting permutation: wait_before_lock change check2 wakeup_before_lock check1 +injection_points_attach +----------------------- + +(1 row) + +step wait_before_lock: + REPACK (CONCURRENTLY) repack_test; + +step change: + UPDATE repack_test SET j=get_long_string() where i=2; + DELETE FROM repack_test WHERE i=3; + INSERT INTO repack_test(i, j) VALUES (4, get_long_string()); + UPDATE repack_test SET i=3 where i=1; + +step check2: + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; + +step wakeup_before_lock: + SELECT injection_points_wakeup('repack-concurrently-before-lock'); + +injection_points_wakeup +----------------------- + +(1 row) + +step wait_before_lock: <... completed> +step check1: + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; + +count +----- + 4 +(1 row) + +count +----- + 0 +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index fcc85414515..a414abb924b 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -45,6 +45,8 @@ tests += { 'specs': [ 'basic', 'inplace', + 'repack', + 'repack_toast', 'syscache-update-pruned', 'heap_lock_update', ], diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec new file mode 100644 index 00000000000..d727a9b056b --- /dev/null +++ b/src/test/modules/injection_points/specs/repack.spec @@ -0,0 +1,142 @@ +# REPACK (CONCURRENTLY) ... USING INDEX ...; +setup +{ + CREATE EXTENSION injection_points; + + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); + + CREATE TABLE relfilenodes(node oid); + + CREATE TABLE data_s1(i int, j int); + CREATE TABLE data_s2(i int, j int); +} + +teardown +{ + DROP TABLE repack_test; + DROP EXTENSION injection_points; + + DROP TABLE relfilenodes; + DROP TABLE data_s1; + DROP TABLE data_s2; +} + +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('repack-concurrently-before-lock', 'wait'); +} +# Perform the initial load and wait for s2 to do some data changes. +step wait_before_lock +{ + REPACK (CONCURRENTLY) repack_test USING INDEX repack_test_pkey; +} +# Check the table from the perspective of s1. +# +# Besides the contents, we also check that relfilenode has changed. + +# Have each session write the contents into a table and use FULL JOIN to check +# if the outputs are identical. +step check1 +{ + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; +} +teardown +{ + SELECT injection_points_detach('repack-concurrently-before-lock'); +} + +session s2 +# Change the existing data. UPDATE changes both key and non-key columns. Also +# update one row twice to test whether tuple version generated by this session +# can be found. +step change_existing +{ + UPDATE repack_test SET i=10 where i=1; + UPDATE repack_test SET j=20 where i=2; + UPDATE repack_test SET i=30 where i=3; + UPDATE repack_test SET i=40 where i=30; + DELETE FROM repack_test WHERE i=4; +} +# Insert new rows and UPDATE / DELETE some of them. Again, update both key and +# non-key column. +step change_new +{ + INSERT INTO repack_test(i, j) VALUES (5, 5), (6, 6), (7, 7), (8, 8); + UPDATE repack_test SET i=50 where i=5; + UPDATE repack_test SET j=60 where i=6; + DELETE FROM repack_test WHERE i=7; +} + +# When applying concurrent data changes, we should see the effects of an +# in-progress subtransaction. +# +# XXX Not sure this test is useful now - it was designed for the patch that +# preserves tuple visibility and which therefore modifies +# TransactionIdIsCurrentTransactionId(). +step change_subxact1 +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (100, 100); + SAVEPOINT s1; + UPDATE repack_test SET i=101 where i=100; + SAVEPOINT s2; + UPDATE repack_test SET i=102 where i=101; + COMMIT; +} + +# When applying concurrent data changes, we should not see the effects of a +# rolled back subtransaction. +# +# XXX Is this test useful? See above. +step change_subxact2 +{ + BEGIN; + SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 110); + ROLLBACK TO SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 111); + COMMIT; +} + +# Check the table from the perspective of s2. +step check2 +{ + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; +} +step wakeup_before_lock +{ + SELECT injection_points_wakeup('repack-concurrently-before-lock'); +} + +# Test if data changes introduced while one session is performing REPACK +# CONCURRENTLY find their way into the table. +permutation + wait_before_lock + change_existing + change_new + change_subxact1 + change_subxact2 + check2 + wakeup_before_lock + check1 diff --git a/src/test/modules/injection_points/specs/repack_toast.spec b/src/test/modules/injection_points/specs/repack_toast.spec new file mode 100644 index 00000000000..b878b198971 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_toast.spec @@ -0,0 +1,112 @@ +# REPACK (CONCURRENTLY); +# +# Test handling of TOAST. At the same time, no tuplesort. +setup +{ + CREATE EXTENSION injection_points; + + -- Return a string that needs to be TOASTed. + CREATE FUNCTION get_long_string() + RETURNS text + LANGUAGE sql as $$ + SELECT string_agg(chr(65 + trunc(25 * random())::int), '') + FROM generate_series(1, 2048) s(x); + $$; + + CREATE TABLE repack_test(i int PRIMARY KEY, j text); + INSERT INTO repack_test(i, j) VALUES (1, get_long_string()), + (2, get_long_string()), (3, get_long_string()); + + CREATE TABLE relfilenodes(node oid); + + CREATE TABLE data_s1(i int, j text); + CREATE TABLE data_s2(i int, j text); +} + +teardown +{ + DROP TABLE repack_test; + DROP EXTENSION injection_points; + DROP FUNCTION get_long_string(); + + DROP TABLE relfilenodes; + DROP TABLE data_s1; + DROP TABLE data_s2; +} + +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('repack-concurrently-before-lock', 'wait'); +} +# Perform the initial load and wait for s2 to do some data changes. +step wait_before_lock +{ + REPACK (CONCURRENTLY) repack_test; +} +# Check the table from the perspective of s1. +# +# Besides the contents, we also check that relfilenode has changed. + +# Have each session write the contents into a table and use FULL JOIN to check +# if the outputs are identical. +step check1 +{ + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; +} +teardown +{ + SELECT injection_points_detach('repack-concurrently-before-lock'); +} + +session s2 +step change +# Separately test UPDATE of both plain ("i") and TOASTed ("j") attribute. In +# the first case, the new tuple we get from reorderbuffer.c contains "j" as a +# TOAST pointer, which we need to update so it points to the new heap. In the +# latter case, we receive "j" as "external indirect" value - here we test that +# the decoding worker writes the tuple to a file correctly and that the +# backend executing REPACK manages to restore it. +{ + UPDATE repack_test SET j=get_long_string() where i=2; + DELETE FROM repack_test WHERE i=3; + INSERT INTO repack_test(i, j) VALUES (4, get_long_string()); + UPDATE repack_test SET i=3 where i=1; +} +# Check the table from the perspective of s2. +step check2 +{ + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; +} +step wakeup_before_lock +{ + SELECT injection_points_wakeup('repack-concurrently-before-lock'); +} + +# Test if data changes introduced while one session is performing REPACK +# CONCURRENTLY find their way into the table. +permutation + wait_before_lock + change + check2 + wakeup_before_lock + check1 diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 2b3cf6d8569..84d61f3ab34 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -2021,7 +2021,7 @@ pg_stat_progress_cluster| SELECT pid, phase, repack_index_relid AS cluster_index_relid, heap_tuples_scanned, - heap_tuples_written, + (heap_tuples_inserted + heap_tuples_updated) AS heap_tuples_written, heap_blks_total, heap_blks_scanned, index_rebuild_count @@ -2101,17 +2101,20 @@ pg_stat_progress_repack| SELECT s.pid, WHEN 2 THEN 'index scanning heap'::text WHEN 3 THEN 'sorting tuples'::text WHEN 4 THEN 'writing new heap'::text - WHEN 5 THEN 'swapping relation files'::text - WHEN 6 THEN 'rebuilding index'::text - WHEN 7 THEN 'performing final cleanup'::text + WHEN 5 THEN 'catch-up'::text + WHEN 6 THEN 'swapping relation files'::text + WHEN 7 THEN 'rebuilding index'::text + WHEN 8 THEN 'performing final cleanup'::text ELSE NULL::text END AS phase, (s.param3)::oid AS repack_index_relid, s.param4 AS heap_tuples_scanned, - s.param5 AS heap_tuples_written, - s.param6 AS heap_blks_total, - s.param7 AS heap_blks_scanned, - s.param8 AS index_rebuild_count + s.param5 AS heap_tuples_inserted, + s.param6 AS heap_tuples_updated, + s.param7 AS heap_tuples_deleted, + s.param8 AS heap_blks_total, + s.param9 AS heap_blks_scanned, + s.param10 AS index_rebuild_count FROM (pg_stat_get_progress_info('REPACK'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20) LEFT JOIN pg_database d ON ((s.datid = d.oid))); pg_stat_progress_vacuum| SELECT s.pid, diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 91b1225da82..90b548cb5d3 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -430,6 +430,7 @@ CatCacheHeader CatalogId CatalogIdMapEntry CatalogIndexState +ChangeContext ChangeVarNodes_callback ChangeVarNodes_context ChannelName @@ -507,6 +508,7 @@ CompressFileHandle CompressionLocation CompressorState ComputeXidHorizonsResult +ConcurrentChangeKind ConditionVariable ConditionVariableMinimallyPadded ConditionalStack @@ -648,6 +650,8 @@ DeclareCursorStmt DecodedBkpBlock DecodedXLogRecord DecodingOutputState +DecodingWorker +DecodingWorkerShared DefElem DefElemAction DefaultACLInfo @@ -1311,6 +1315,7 @@ IndexElem IndexFetchHeapData IndexFetchTableData IndexInfo +IndexInsertState IndexList IndexOnlyScan IndexOnlyScanState @@ -2626,6 +2631,7 @@ ReorderBufferTupleCidKey ReorderBufferUpdateProgressTxnCB ReorderTuple RepackCommand +RepackDecodingState RepackStmt ReparameterizeForeignPathByChild_function ReplOriginId -- 2.47.3 --r2slln3zpmilwu22 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v51-0004-Fix-a-few-problems-in-index-build-progress-repor.patch" From fa49c6add8385862e818688cf420b5986dc4b9ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Fri, 27 Mar 2026 16:50:06 +0100 Subject: [PATCH v51 04/10] Fix a few problems in index build progress reporting. First, index_build() should not update the progress when being driven by REPACK, because the progress reporting infractructure cannot handle status of two commands at the same time. So far, REPACK with the CONCURRENTLY option neglected this problem altogether, but even the existing REPACK wasn't consistent enough: even if the 'progress' variable in repack_index() was false, it didn't pass the value to index_build(). Second, REPACK (CONCURRENTLY) should not set PROGRESS_REPACK_PHASE to PROGRESS_REPACK_PHASE_FINAL_CLEANUP in rebuild_relation() because it calls finish_heap_swap() anyway (via rebuild_relation_finish_concurrent()), which does the same thing. --- src/backend/bootstrap/bootstrap.c | 2 +- src/backend/catalog/heap.c | 3 ++- src/backend/catalog/index.c | 22 ++++++++++++++++++---- src/backend/catalog/toasting.c | 3 ++- src/backend/commands/indexcmds.c | 1 + src/include/catalog/index.h | 4 +++- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c index 38ef683d4c7..60fb7051830 100644 --- a/src/backend/bootstrap/bootstrap.c +++ b/src/backend/bootstrap/bootstrap.c @@ -1183,7 +1183,7 @@ build_indices(void) heap = table_open(ILHead->il_heap, NoLock); ind = index_open(ILHead->il_ind, NoLock); - index_build(heap, ind, ILHead->il_info, false, false); + index_build(heap, ind, ILHead->il_info, false, false, false); index_close(ind, NoLock); table_close(heap, NoLock); diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 5748aa9a1a9..ae6b7cda3dd 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -3570,7 +3570,8 @@ RelationTruncateIndexes(Relation heapRelation) /* Initialize the index and rebuild */ /* Note: we do not need to re-establish pkey setting */ - index_build(heapRelation, currentIndex, indexInfo, true, false); + index_build(heapRelation, currentIndex, indexInfo, true, false, + true); /* We're done with this index */ index_close(currentIndex, NoLock); diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index e418d67e8e4..4b8ed2c7660 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -715,6 +715,9 @@ UpdateIndexRelation(Oid indexoid, * already exists. * INDEX_CREATE_PARTITIONED: * create a partitioned index (table must be partitioned) + * INDEX_CREATE_REPORT_PROGRESS: + * update the backend's progress information during index build. + * constr_flags: flags passed to index_constraint_create * (only if INDEX_CREATE_ADD_CONSTRAINT is set) * allow_system_table_mods: allow table to be a system catalog @@ -760,6 +763,7 @@ index_create(Relation heapRelation, bool invalid = (flags & INDEX_CREATE_INVALID) != 0; bool concurrent = (flags & INDEX_CREATE_CONCURRENT) != 0; bool partitioned = (flags & INDEX_CREATE_PARTITIONED) != 0; + bool progress = (flags & INDEX_CREATE_REPORT_PROGRESS) != 0; char relkind; TransactionId relfrozenxid; MultiXactId relminmxid; @@ -1276,7 +1280,8 @@ index_create(Relation heapRelation, } else { - index_build(heapRelation, indexRelation, indexInfo, false, true); + index_build(heapRelation, indexRelation, indexInfo, false, true, + progress); } /* @@ -1448,6 +1453,12 @@ index_create_copy(Relation heapRelation, bool concurrently, stattargets[i].isnull = isnull; } + /* + * Note: The current callers do not need INDEX_CREATE_REPORT_PROGRESS. If + * 'concurrently' is true, there is no build at all. Otherwise the index + * build is a sub-command of REPACK. The current infrastructure does not + * allow two commands to report their progress at the same time. + */ if (concurrently) flags = INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT; @@ -1538,7 +1549,7 @@ index_concurrently_build(Oid heapRelationId, indexInfo->ii_BrokenHotChain = false; /* Now build the index */ - index_build(heapRel, indexRelation, indexInfo, false, true); + index_build(heapRel, indexRelation, indexInfo, false, true, true); /* Roll back any GUC changes executed by index functions */ AtEOXact_GUC(false, save_nestlevel); @@ -3009,6 +3020,7 @@ index_update_stats(Relation rel, * * isreindex indicates we are recreating a previously-existing index. * parallel indicates if parallelism may be useful. + * progress indicates if the backend should update its progress info. * * Note: before Postgres 8.2, the passed-in heap and index Relations * were automatically closed by this routine. This is no longer the case. @@ -3019,7 +3031,8 @@ index_build(Relation heapRelation, Relation indexRelation, IndexInfo *indexInfo, bool isreindex, - bool parallel) + bool parallel, + bool progress) { IndexBuildResult *stats; Oid save_userid; @@ -3070,6 +3083,7 @@ index_build(Relation heapRelation, RestrictSearchPath(); /* Set up initial progress report status */ + if (progress) { const int progress_index[] = { PROGRESS_CREATEIDX_PHASE, @@ -3827,7 +3841,7 @@ reindex_index(const ReindexStmt *stmt, Oid indexId, /* Initialize the index and rebuild */ /* Note: we do not need to re-establish pkey setting */ - index_build(heapRelation, iRel, indexInfo, true, true); + index_build(heapRelation, iRel, indexInfo, true, true, progress); /* Re-allow use of target index */ ResetReindexProcessing(); diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c index 4aa52a4bd25..51b27a8c71c 100644 --- a/src/backend/catalog/toasting.c +++ b/src/backend/catalog/toasting.c @@ -332,7 +332,8 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid, BTREE_AM_OID, rel->rd_rel->reltablespace, collationIds, opclassIds, NULL, coloptions, NULL, (Datum) 0, - INDEX_CREATE_IS_PRIMARY, 0, true, true, NULL); + INDEX_CREATE_IS_PRIMARY | INDEX_CREATE_REPORT_PROGRESS, 0, + true, true, NULL); table_close(toast_rel, NoLock); diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index cba379810c7..932924c13e0 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -1231,6 +1231,7 @@ DefineIndex(ParseState *pstate, flags |= INDEX_CREATE_PARTITIONED; if (stmt->primary) flags |= INDEX_CREATE_IS_PRIMARY; + flags |= INDEX_CREATE_REPORT_PROGRESS; /* * If the table is partitioned, and recursion was declined but partitions diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h index ed9e4c37d27..7ebe4f0bd87 100644 --- a/src/include/catalog/index.h +++ b/src/include/catalog/index.h @@ -71,6 +71,7 @@ extern void index_check_primary_key(Relation heapRel, #define INDEX_CREATE_IF_NOT_EXISTS (1 << 4) #define INDEX_CREATE_PARTITIONED (1 << 5) #define INDEX_CREATE_INVALID (1 << 6) +#define INDEX_CREATE_REPORT_PROGRESS (1 << 7) extern Oid index_create(Relation heapRelation, const char *indexRelationName, @@ -148,7 +149,8 @@ extern void index_build(Relation heapRelation, Relation indexRelation, IndexInfo *indexInfo, bool isreindex, - bool parallel); + bool parallel, + bool progress); extern void validate_index(Oid heapId, Oid indexId, Snapshot snapshot); -- 2.47.3 --r2slln3zpmilwu22 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v51-0005-invert-meaning-of-index_create-flag-bit.patch" From 0491b1a0292d91765b267a38baf8ab75b8b269f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Fri, 3 Apr 2026 21:08:18 +0200 Subject: [PATCH v51 05/10] invert meaning of index_create flag bit --- src/backend/catalog/index.c | 20 +++++++++++--------- src/backend/catalog/toasting.c | 2 +- src/backend/commands/indexcmds.c | 1 - src/include/catalog/index.h | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 4b8ed2c7660..5a7c9d81917 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -715,9 +715,9 @@ UpdateIndexRelation(Oid indexoid, * already exists. * INDEX_CREATE_PARTITIONED: * create a partitioned index (table must be partitioned) - * INDEX_CREATE_REPORT_PROGRESS: - * update the backend's progress information during index build. - + * INDEX_CREATE_SUPPRESS_PROGRESS: + * don't report progress during the index build. + * * constr_flags: flags passed to index_constraint_create * (only if INDEX_CREATE_ADD_CONSTRAINT is set) * allow_system_table_mods: allow table to be a system catalog @@ -763,7 +763,7 @@ index_create(Relation heapRelation, bool invalid = (flags & INDEX_CREATE_INVALID) != 0; bool concurrent = (flags & INDEX_CREATE_CONCURRENT) != 0; bool partitioned = (flags & INDEX_CREATE_PARTITIONED) != 0; - bool progress = (flags & INDEX_CREATE_REPORT_PROGRESS) != 0; + bool progress = (flags & INDEX_CREATE_SUPPRESS_PROGRESS) == 0; char relkind; TransactionId relfrozenxid; MultiXactId relminmxid; @@ -1454,13 +1454,15 @@ index_create_copy(Relation heapRelation, bool concurrently, } /* - * Note: The current callers do not need INDEX_CREATE_REPORT_PROGRESS. If - * 'concurrently' is true, there is no build at all. Otherwise the index - * build is a sub-command of REPACK. The current infrastructure does not - * allow two commands to report their progress at the same time. + * The current callers do not need to report progress: if 'concurrently' is + * true, there is no build at all to report about; and otherwise the index + * build is a sub-command of REPACK, and the current progress reporting + * infrastructure does not allow two commands to report their progress at + * the same time. */ if (concurrently) - flags = INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT; + flags = INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT | + INDEX_CREATE_SUPPRESS_PROGRESS; /* * Now create the new index. diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c index 51b27a8c71c..dcee536fd3f 100644 --- a/src/backend/catalog/toasting.c +++ b/src/backend/catalog/toasting.c @@ -332,7 +332,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid, BTREE_AM_OID, rel->rd_rel->reltablespace, collationIds, opclassIds, NULL, coloptions, NULL, (Datum) 0, - INDEX_CREATE_IS_PRIMARY | INDEX_CREATE_REPORT_PROGRESS, 0, + INDEX_CREATE_IS_PRIMARY, 0, true, true, NULL); table_close(toast_rel, NoLock); diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 932924c13e0..cba379810c7 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -1231,7 +1231,6 @@ DefineIndex(ParseState *pstate, flags |= INDEX_CREATE_PARTITIONED; if (stmt->primary) flags |= INDEX_CREATE_IS_PRIMARY; - flags |= INDEX_CREATE_REPORT_PROGRESS; /* * If the table is partitioned, and recursion was declined but partitions diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h index 7ebe4f0bd87..9f538dac798 100644 --- a/src/include/catalog/index.h +++ b/src/include/catalog/index.h @@ -71,7 +71,7 @@ extern void index_check_primary_key(Relation heapRel, #define INDEX_CREATE_IF_NOT_EXISTS (1 << 4) #define INDEX_CREATE_PARTITIONED (1 << 5) #define INDEX_CREATE_INVALID (1 << 6) -#define INDEX_CREATE_REPORT_PROGRESS (1 << 7) +#define INDEX_CREATE_SUPPRESS_PROGRESS (1 << 7) extern Oid index_create(Relation heapRelation, const char *indexRelationName, -- 2.47.3 --r2slln3zpmilwu22 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v51-0006-Error-out-any-process-that-would-block-at-REPACK.patch" From 3785d633cbb39d19e350939b2c570d809deeebc9 Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Wed, 1 Apr 2026 17:35:47 +0200 Subject: [PATCH v51 06/10] Error out any process that would block at REPACK Any process waiting on REPACK to release its lock would actually cause it to deadlock when it tries to upgrade its lock to AEL, losing all work done to that point. We avoid this by teaching the deadlock detector to raise an error when this condition is detected. --- src/backend/commands/repack.c | 60 ++++++++++---- src/backend/storage/lmgr/deadlock.c | 15 ++++ src/include/storage/proc.h | 6 +- src/test/modules/injection_points/Makefile | 1 + .../expected/repack_deadlock.out | 63 ++++++++++++++ src/test/modules/injection_points/meson.build | 1 + .../specs/repack_deadlock.spec | 83 +++++++++++++++++++ 7 files changed, 210 insertions(+), 19 deletions(-) create mode 100644 src/test/modules/injection_points/expected/repack_deadlock.out create mode 100644 src/test/modules/injection_points/specs/repack_deadlock.spec diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 03829892d57..d6e446d582d 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -279,6 +279,21 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) /* Determine the lock mode to use. */ lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0); + /* + * If in concurrent mode, set the PROC_IN_CONCURRENT_REPACK flag. This + * makes the deadlock checker cause anyone that would conflict with us to + * error out. It's important to set this flag ahead of actually locking + * the relation; it won't of course affect anyone until we do have a lock + * that others can conflict with. + */ + if ((params.options & CLUOPT_CONCURRENT) != 0) + { + LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); + MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK; + ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags; + LWLockRelease(ProcArrayLock); + } + /* * If a single relation is specified, process it and we're done ... unless * the relation is a partitioned table, in which case we fall through. @@ -479,11 +494,8 @@ RepackLockLevel(bool concurrent) * If indexOid is InvalidOid, the table will be rewritten in physical order * instead of index order. * - * Note that, in the concurrent case, the function releases the lock at some - * point, in order to get AccessExclusiveLock for the final steps (i.e. to - * swap the relation files). To make things simpler, the caller should expect - * OldHeap to be closed on return, regardless CLUOPT_CONCURRENT. (The - * AccessExclusiveLock is kept till the end of the transaction.) + * On return, OldHeap is closed but locked with AccessExclusiveLock - the lock + * will be released at end of the transaction. * * 'cmd' indicates which command is being executed, to be used for error * messages. @@ -515,10 +527,12 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, /* * Make sure we're not in a transaction block. * - * The reason is that repack_setup_logical_decoding() could deadlock - * if there's an XID already assigned. It would be possible to run in - * a transaction block if we had no XID, but this restriction is - * simpler for users to understand and we don't lose anything. + * The reason is that repack_setup_logical_decoding() could wait + * indefinitely for our XID to complete. (The deadlock detector would + * not recognize it because we'd be waiting for ourselves, i.e. no + * real lock conflict.) It would be possible to run in a transaction + * block if we had no XID, but this restriction is simpler for users + * to understand and we don't lose anything. */ PreventInTransactionBlock(isTopLevel, "REPACK (CONCURRENTLY)"); @@ -1001,10 +1015,8 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, * Note that the worker has to wait for all transactions with XID * already assigned to finish. If some of those transactions is * waiting for a lock conflicting with ShareUpdateExclusiveLock on our - * table (e.g. it runs CREATE INDEX), we can end up in a deadlock. - * Not sure this risk is worth unlocking/locking the table (and its - * clustering index) and checking again if it's still eligible for - * REPACK CONCURRENTLY. + * table (e.g. it runs CREATE INDEX), it should encounter ERROR in the + * deadlock checking code. */ start_repack_decoding_worker(tableOid); @@ -3093,7 +3105,19 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock); /* - * Tuples and pages of the old heap will be gone, but the heap will stay. + * Now that we have all access-exclusive locks on all relations, we no + * longer want other processes to error out when trying to acquire a + * conflicting lock. Therefore, unset our flag. + */ + LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); + MyProc->statusFlags &= ~PROC_IN_CONCURRENT_REPACK; + ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags; + LWLockRelease(ProcArrayLock); + + /* + * Tuples and pages of the old heap will be gone, but the heap itself will + * stay. In order for predicate locks to continue to work, convert them + * to relation-level locks. We do this both for table and indexes. */ TransferPredicateLocksToHeapRelation(OldHeap); foreach_ptr(RelationData, index, indexrels) @@ -3366,9 +3390,11 @@ start_repack_decoding_worker(Oid relid) /* * The decoding setup must be done before the caller can have XID assigned - * for any reason, otherwise the worker might end up in a deadlock, - * waiting for the caller's transaction to end. Therefore wait here until - * the worker indicates that it has the logical decoding initialized. + * for any reason, otherwise the worker might end up waiting for the + * caller's transaction to end. (Deadlock detector does not consider this + * a conflict because the worker is in the same locking group as the + * backend that launched it.) Therefore wait here until the worker + * indicates that it has the logical decoding initialized. */ ConditionVariablePrepareToSleep(&shared->cv); for (;;) diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c index b8962d875b6..c20ac682b0d 100644 --- a/src/backend/storage/lmgr/deadlock.c +++ b/src/backend/storage/lmgr/deadlock.c @@ -620,6 +620,21 @@ FindLockCycleRecurseMember(PGPROC *checkProc, proc->statusFlags & PROC_IS_AUTOVACUUM) blocking_autovacuum_proc = proc; + /* + * Similarly, if we note that we're blocked by some + * process running REPACK (CONCURRENTLY), just fail. That + * process is going to upgrade its lock at some point, and + * it would be inappropriate for any other process to + * cause that to fail. + */ + if (checkProc == MyProc && + proc->statusFlags & PROC_IN_CONCURRENT_REPACK) + ereport(ERROR, + errcode(ERRCODE_OBJECT_IN_USE), + errmsg("could not wait for concurrent REPACK"), + errdetail("Process %d waits for REPACK running on process %d", + MyProc->pid, proc->pid)); + /* We're done looking at this proclock */ break; } diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 1dad125706e..8ad9718f3d6 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -69,10 +69,12 @@ struct XidCache #define PROC_AFFECTS_ALL_HORIZONS 0x20 /* this proc's xmin must be * included in vacuum horizons * in all databases */ +#define PROC_IN_CONCURRENT_REPACK 0x40 /* REPACK (CONCURRENTLY) */ -/* flags reset at EOXact */ +/* flags reset at EOXact. A bit of a misnomer ... */ #define PROC_VACUUM_STATE_MASK \ - (PROC_IN_VACUUM | PROC_IN_SAFE_IC | PROC_VACUUM_FOR_WRAPAROUND) + (PROC_IN_VACUUM | PROC_IN_SAFE_IC | PROC_VACUUM_FOR_WRAPAROUND | \ + PROC_IN_CONCURRENT_REPACK) /* * Xmin-related flags. Make sure any flags that affect how the process' Xmin diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index 2cd7d87c533..f7663859fe2 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -15,6 +15,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress ISOLATION = basic \ inplace \ repack \ + repack_deadlock \ repack_toast \ syscache-update-pruned \ heap_lock_update diff --git a/src/test/modules/injection_points/expected/repack_deadlock.out b/src/test/modules/injection_points/expected/repack_deadlock.out new file mode 100644 index 00000000000..a86e4767536 --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_deadlock.out @@ -0,0 +1,63 @@ +Parsed test spec with 2 sessions + +starting permutation: wait_before_lock add_column wakeup_before_lock check1 +injection_points_attach +----------------------- + +(1 row) + +step wait_before_lock: + REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey; + +step add_column: + alter table repack_deadlock add column noise text; + +step add_column: <... completed> +ERROR: could not wait for concurrent REPACK +step wakeup_before_lock: + SELECT injection_points_wakeup('repack-concurrently-before-lock'); + +injection_points_wakeup +----------------------- + +(1 row) + +step wait_before_lock: <... completed> +step check1: + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + SELECT i, j FROM repack_deadlock ORDER BY i, j; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_deadlock; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; + +count +----- + 1 +(1 row) + +i|j +-+- +1|1 +2|2 +3|3 +4|4 +(4 rows) + +count +----- + 4 +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index a414abb924b..1cd88d6db65 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -46,6 +46,7 @@ tests += { 'basic', 'inplace', 'repack', + 'repack_deadlock', 'repack_toast', 'syscache-update-pruned', 'heap_lock_update', diff --git a/src/test/modules/injection_points/specs/repack_deadlock.spec b/src/test/modules/injection_points/specs/repack_deadlock.spec new file mode 100644 index 00000000000..9d23a6588c2 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_deadlock.spec @@ -0,0 +1,83 @@ +# Test REPACK with a concurrent transaction that would cause a deadlock +setup +{ + CREATE EXTENSION injection_points; + + CREATE TABLE repack_deadlock(i int PRIMARY KEY, j int); + INSERT INTO repack_deadlock(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); + + CREATE TABLE relfilenodes(node oid); + + CREATE TABLE data_s1(i int, j int); + CREATE TABLE data_s2(i int, j int); +} + +teardown +{ + DROP TABLE repack_deadlock; + DROP EXTENSION injection_points; + + DROP TABLE relfilenodes; + DROP TABLE data_s1; + DROP TABLE data_s2; +} + +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('repack-concurrently-before-lock', 'wait'); +} +# Perform the initial load and wait for s2 to do some data changes. +step wait_before_lock +{ + REPACK (CONCURRENTLY) repack_deadlock USING INDEX repack_deadlock_pkey; +} +# Check the table from the perspective of s1. +# +# Besides the contents, we also check that relfilenode has changed. + +# Have each session write the contents into a table and use FULL JOIN to check +# if the outputs are identical. +step check1 +{ + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_deadlock'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + SELECT i, j FROM repack_deadlock ORDER BY i, j; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_deadlock; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; +} +teardown +{ + SELECT injection_points_detach('repack-concurrently-before-lock'); +} + +session s2 +# Change the existing data. UPDATE changes both key and non-key columns. Also +# update one row twice to test whether tuple version generated by this session +# can be found. +step add_column +{ + alter table repack_deadlock add column noise text; +} + +step wakeup_before_lock +{ + SELECT injection_points_wakeup('repack-concurrently-before-lock'); +} + +# Test if data changes introduced while one session is performing REPACK +# CONCURRENTLY find their way into the table. +permutation + wait_before_lock + add_column + wakeup_before_lock + check1 -- 2.47.3 --r2slln3zpmilwu22 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v51-0007-Check-for-transaction-block-early-in-ExecRepack.patch" From dbf974f260878728512a413901953135925c2f55 Mon Sep 17 00:00:00 2001 From: Srinath Reddy Sadipiralla Date: Fri, 3 Apr 2026 20:53:56 +0530 Subject: [PATCH v51 07/10] Check for transaction block early in ExecRepack Currently, executing REPACK (CONCURRENTLY) without a table name inside a transaction block throws the error "REPACK CONCURRENTLY requires explicit table name" instead of the expected transaction block error. This occurs because ExecRepack() validates the parsed options and missing relation before verifying the transaction state. This behavior is inconsistent with other utility commands like VACUUM ,REINDEX, etc; which invoke PreventInTransactionBlock() at the very start of their execution to properly reject execution inside user transactions before validating targets. Add PreventInTransactionBlock to the top of ExecRepack() to enforce the transaction block restriction early. This prevents the user from fixing a missing table error only to immediately hit a transaction block error, and also ensures consistency with rest of the commands. Author: Srinath Reddy Sadipiralla --- src/backend/commands/repack.c | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index d6e446d582d..d4c1f0e7652 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -292,6 +292,18 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) MyProc->statusFlags |= PROC_IN_CONCURRENT_REPACK; ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags; LWLockRelease(ProcArrayLock); + + /* + * Make sure we're not in a transaction block. + * + * The reason is that repack_setup_logical_decoding() could wait + * indefinitely for our XID to complete. (The deadlock detector would + * not recognize it because we'd be waiting for ourselves, i.e. no + * real lock conflict.) It would be possible to run in a transaction + * block if we had no XID, but this restriction is simpler for users + * to understand and we don't lose anything. + */ + PreventInTransactionBlock(isTopLevel, "REPACK (CONCURRENTLY)"); } /* @@ -523,21 +535,7 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, * replica index OID. */ if (concurrent) - { - /* - * Make sure we're not in a transaction block. - * - * The reason is that repack_setup_logical_decoding() could wait - * indefinitely for our XID to complete. (The deadlock detector would - * not recognize it because we'd be waiting for ourselves, i.e. no - * real lock conflict.) It would be possible to run in a transaction - * block if we had no XID, but this restriction is simpler for users - * to understand and we don't lose anything. - */ - PreventInTransactionBlock(isTopLevel, "REPACK (CONCURRENTLY)"); - check_repack_concurrently_requirements(OldHeap, &ident_idx); - } /* Check for user-requested abort. */ CHECK_FOR_INTERRUPTS(); -- 2.47.3 --r2slln3zpmilwu22 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v51-0008-Introduce-an-option-to-make-logical-replication-.patch" From 418f8dd2d3924e4aaee6a6288c21d77a1faed647 Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Fri, 3 Apr 2026 12:34:04 +0200 Subject: [PATCH v51 08/10] Introduce an option to make logical replication database specific. By default, the logical decoding assumes access to shared catalogs, so the snapshot builder needs to consider cluster-wide XIDs during startup. That in turn means that, if any transaction is already running (and has XID assigned), the snapshot builder needs to wait for its completion, as it does not know if that transaction performed catalog changes earlier. Possible problem with this concept is that if REPACK (CONCURRENTLY) is running in some database, backends running the same command in other databases get stuck until the first one has committed. Thus only as single backend in the cluster can run REPACK (CONCURRENTLY) at any time. Likewise, REPACK (CONCURRENTLY) can block walsenders starting on behalf of subscriptions throughout the cluster. This patch introduces the possibility for a backend to declare that its output plugin does not use shared catalogs (i.e. catalogs that can be changed by transactions running in other databases in the cluster). In that case, no snapshot the backend will use during the decoding needs to contain information about transactions running in other databases. Thus the snapshot builder only needs to wait for completion of transactions in the current database. Currently we only use this option in the REPACK background worker. It could possibly be used in walsender involved in logical replication too, however that would need thorough analysis of its output plugin. The patch bumps WAL version number, due to a new field in xl_running_xacts. --- contrib/pg_visibility/pg_visibility.c | 4 ++-- src/backend/access/index/genam.c | 18 ++++++++++++++++ src/backend/access/rmgrdesc/standbydesc.c | 2 ++ src/backend/access/transam/xlog.c | 2 +- src/backend/access/transam/xlogfuncs.c | 2 +- src/backend/commands/repack_worker.c | 8 +++++++ src/backend/postmaster/bgwriter.c | 2 +- src/backend/replication/logical/snapbuild.c | 10 ++++++++- src/backend/replication/slot.c | 12 +++++++++-- src/backend/storage/ipc/procarray.c | 23 ++++++++++++++++++++- src/backend/storage/ipc/standby.c | 14 +++++++++++-- src/include/access/genam.h | 8 +++++++ src/include/access/xlog_internal.h | 2 +- src/include/storage/procarray.h | 2 +- src/include/storage/standby.h | 3 ++- src/include/storage/standbydefs.h | 1 + 16 files changed, 99 insertions(+), 14 deletions(-) diff --git a/contrib/pg_visibility/pg_visibility.c b/contrib/pg_visibility/pg_visibility.c index dfab0b64cf5..d564bd2a00c 100644 --- a/contrib/pg_visibility/pg_visibility.c +++ b/contrib/pg_visibility/pg_visibility.c @@ -621,7 +621,7 @@ GetStrictOldestNonRemovableTransactionId(Relation rel) else if (rel == NULL || rel->rd_rel->relisshared) { /* Shared relation: take into account all running xids */ - runningTransactions = GetRunningTransactionData(); + runningTransactions = GetRunningTransactionData(InvalidOid); LWLockRelease(ProcArrayLock); LWLockRelease(XidGenLock); return runningTransactions->oldestRunningXid; @@ -632,7 +632,7 @@ GetStrictOldestNonRemovableTransactionId(Relation rel) * Normal relation: take into account xids running within the current * database */ - runningTransactions = GetRunningTransactionData(); + runningTransactions = GetRunningTransactionData(InvalidOid); LWLockRelease(ProcArrayLock); LWLockRelease(XidGenLock); return runningTransactions->oldestDatabaseRunningXid; diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c index 1408989c568..df092dc999a 100644 --- a/src/backend/access/index/genam.c +++ b/src/backend/access/index/genam.c @@ -37,6 +37,14 @@ #include "utils/ruleutils.h" #include "utils/snapmgr.h" +/* + * If a backend is going to do logical decoding and if the output plugin does + * not need to access shared catalogs, setting this variable to false can make + * the decoding startup faster. In particular, the backend will not need to + * wait for completion of already running transactions in other databases. + */ +bool accessSharedCatalogsInDecoding = true; + /* ---------------------------------------------------------------- * general access method routines @@ -394,6 +402,16 @@ systable_beginscan(Relation heapRelation, SysScanDesc sysscan; Relation irel; + /* + * If this backend promised that it won't access shared catalogs during + * logical decoding, this seems to be the right place to check. + * + * XXX Should this be ereport(ERROR) ? + */ + Assert(!HistoricSnapshotActive() || + accessSharedCatalogsInDecoding || + !heapRelation->rd_rel->relisshared); + if (indexOK && !IgnoreSystemIndexes && !ReindexIsProcessingIndex(indexId)) diff --git a/src/backend/access/rmgrdesc/standbydesc.c b/src/backend/access/rmgrdesc/standbydesc.c index 0a291354ae2..685d1bdb024 100644 --- a/src/backend/access/rmgrdesc/standbydesc.c +++ b/src/backend/access/rmgrdesc/standbydesc.c @@ -41,6 +41,8 @@ standby_desc_running_xacts(StringInfo buf, xl_running_xacts *xlrec) for (i = 0; i < xlrec->subxcnt; i++) appendStringInfo(buf, " %u", xlrec->xids[xlrec->xcnt + i]); } + + appendStringInfo(buf, "; dbid: %u", xlrec->dbid); } void diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 2c1c6f88b74..8da8eff93e4 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -7336,7 +7336,7 @@ CreateCheckPoint(int flags) * recovery we don't need to write running xact data. */ if (!shutdown && XLogStandbyInfoActive()) - LogStandbySnapshot(); + LogStandbySnapshot(InvalidOid); START_CRIT_SECTION(); diff --git a/src/backend/access/transam/xlogfuncs.c b/src/backend/access/transam/xlogfuncs.c index 65bbaeda59c..0f5979691e6 100644 --- a/src/backend/access/transam/xlogfuncs.c +++ b/src/backend/access/transam/xlogfuncs.c @@ -245,7 +245,7 @@ pg_log_standby_snapshot(PG_FUNCTION_ARGS) (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("pg_log_standby_snapshot() can only be used if \"wal_level\" >= \"replica\""))); - recptr = LogStandbySnapshot(); + recptr = LogStandbySnapshot(InvalidOid); /* * As a convenience, return the WAL location of the last inserted record diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c85166ba849..ff34e246469 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -16,6 +16,7 @@ */ #include "postgres.h" +#include "access/genam.h" #include "access/table.h" #include "access/xlog_internal.h" #include "access/xlogutils.h" @@ -237,6 +238,13 @@ repack_setup_logical_decoding(Oid relid) EnsureLogicalDecodingEnabled(); + /* + * By declaring that our output plugin does not need shared catalogs, we + * avoid waiting for completion of transactions running in other databases + * than the one we're connected to. + */ + accessSharedCatalogsInDecoding = false; + /* * Neither prepare_write nor do_write callback nor update_progress is * useful for us. diff --git a/src/backend/postmaster/bgwriter.c b/src/backend/postmaster/bgwriter.c index 1d8947774a9..a30de4262eb 100644 --- a/src/backend/postmaster/bgwriter.c +++ b/src/backend/postmaster/bgwriter.c @@ -289,7 +289,7 @@ BackgroundWriterMain(const void *startup_data, size_t startup_data_len) if (now >= timeout && last_snapshot_lsn <= GetLastImportantRecPtr()) { - last_snapshot_lsn = LogStandbySnapshot(); + last_snapshot_lsn = LogStandbySnapshot(InvalidOid); last_snapshot_ts = now; } } diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c index b4269a3b102..2e3926e1d13 100644 --- a/src/backend/replication/logical/snapbuild.c +++ b/src/backend/replication/logical/snapbuild.c @@ -125,6 +125,7 @@ #include #include +#include "access/genam.h" #include "access/heapam_xlog.h" #include "access/transam.h" #include "access/xact.h" @@ -1465,7 +1466,14 @@ SnapBuildWaitSnapshot(xl_running_xacts *running, TransactionId cutoff) */ if (!RecoveryInProgress()) { - LogStandbySnapshot(); + Oid dbid; + + /* + * Only consider transactions of the current database if our plugin is + * not supposed to access shared catalogs. + */ + dbid = accessSharedCatalogsInDecoding ? InvalidOid : MyDatabaseId; + LogStandbySnapshot(dbid); } } diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c index a9092fc2382..d553bd5dbff 100644 --- a/src/backend/replication/slot.c +++ b/src/backend/replication/slot.c @@ -39,6 +39,7 @@ #include #include +#include "access/genam.h" #include "access/transam.h" #include "access/xlog_internal.h" #include "access/xlogrecovery.h" @@ -1760,9 +1761,16 @@ ReplicationSlotReserveWal(void) if (!RecoveryInProgress() && SlotIsLogical(slot)) { XLogRecPtr flushptr; + Oid dbid; - /* make sure we have enough information to start */ - flushptr = LogStandbySnapshot(); + /* + * Make sure we have enough information to start. + * + * Only consider transactions of the current database if our plugin is + * not supposed to access shared catalogs. + */ + dbid = accessSharedCatalogsInDecoding ? InvalidOid : MyDatabaseId; + flushptr = LogStandbySnapshot(dbid); /* and make sure it's fsynced to disk */ XLogFlush(flushptr); diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c index cc207cb56e3..92ed34128e5 100644 --- a/src/backend/storage/ipc/procarray.c +++ b/src/backend/storage/ipc/procarray.c @@ -2631,9 +2631,11 @@ ProcArrayInstallRestoredXmin(TransactionId xmin, PGPROC *proc) * * Note that if any transaction has overflowed its cached subtransactions * then there is no real need include any subtransactions. + * + * If 'dbid' is valid, only gather transactions running in that database. */ RunningTransactions -GetRunningTransactionData(void) +GetRunningTransactionData(Oid dbid) { /* result workspace */ static RunningTransactionsData CurrentRunningXactsData; @@ -2708,6 +2710,18 @@ GetRunningTransactionData(void) if (!TransactionIdIsValid(xid)) continue; + /* + * Filter by database OID if requested. + */ + if (OidIsValid(dbid)) + { + int pgprocno = arrayP->pgprocnos[index]; + PGPROC *proc = &allProcs[pgprocno]; + + if (proc->databaseId != dbid) + continue; + } + /* * Be careful not to exclude any xids before calculating the values of * oldestRunningXid and suboverflowed, since these are used to clean @@ -2758,6 +2772,12 @@ GetRunningTransactionData(void) PGPROC *proc = &allProcs[pgprocno]; int nsubxids; + /* + * Filter by database OID if requested. + */ + if (OidIsValid(dbid) && proc->databaseId != dbid) + continue; + /* * Save subtransaction XIDs. Other backends can't add or remove * entries while we're holding XidGenLock. @@ -2791,6 +2811,7 @@ GetRunningTransactionData(void) * increases if slots do. */ + CurrentRunningXacts->dbid = dbid; CurrentRunningXacts->xcnt = count - subcount; CurrentRunningXacts->subxcnt = subcount; CurrentRunningXacts->subxid_status = suboverflowed ? SUBXIDS_IN_SUBTRANS : SUBXIDS_IN_ARRAY; diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c index de9092fdf5b..c653ea742bc 100644 --- a/src/backend/storage/ipc/standby.c +++ b/src/backend/storage/ipc/standby.c @@ -1188,6 +1188,14 @@ standby_redo(XLogReaderState *record) xl_running_xacts *xlrec = (xl_running_xacts *) XLogRecGetData(record); RunningTransactionsData running; + /* + * Records issued for specific database are not suitable for physical + * replication because that affects the whole cluster. In particular, + * the list of XID is probably incomplete here. + */ + if (OidIsValid(xlrec->dbid)) + return; + running.xcnt = xlrec->xcnt; running.subxcnt = xlrec->subxcnt; running.subxid_status = xlrec->subxid_overflow ? SUBXIDS_MISSING : SUBXIDS_IN_ARRAY; @@ -1277,11 +1285,12 @@ standby_redo(XLogReaderState *record) * as there's no independent knob to just enable logical decoding. For * details of how this is used, check snapbuild.c's introductory comment. * + * If 'dbid' is valid, only gather transactions running in that database. * * Returns the RecPtr of the last inserted record. */ XLogRecPtr -LogStandbySnapshot(void) +LogStandbySnapshot(Oid dbid) { XLogRecPtr recptr; RunningTransactions running; @@ -1314,7 +1323,7 @@ LogStandbySnapshot(void) * Log details of all in-progress transactions. This should be the last * record we write, because standby will open up when it sees this. */ - running = GetRunningTransactionData(); + running = GetRunningTransactionData(dbid); /* * GetRunningTransactionData() acquired ProcArrayLock, we must release it. @@ -1358,6 +1367,7 @@ LogCurrentRunningXacts(RunningTransactions CurrRunningXacts) xl_running_xacts xlrec; XLogRecPtr recptr; + xlrec.dbid = CurrRunningXacts->dbid; xlrec.xcnt = CurrRunningXacts->xcnt; xlrec.subxcnt = CurrRunningXacts->subxcnt; xlrec.subxid_overflow = (CurrRunningXacts->subxid_status != SUBXIDS_IN_ARRAY); diff --git a/src/include/access/genam.h b/src/include/access/genam.h index b69320a7fc8..56c573399ab 100644 --- a/src/include/access/genam.h +++ b/src/include/access/genam.h @@ -136,6 +136,14 @@ typedef struct IndexOrderByDistance bool isnull; } IndexOrderByDistance; +/* + * Is the backend interested in shared catalogs when performing logical + * decoding? + * + * XXX Is there a better place for this declaration? + */ +extern bool accessSharedCatalogsInDecoding; + /* * generalized index_ interface routines (in indexam.c) */ diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h index 755835d63bf..ae19982d88d 100644 --- a/src/include/access/xlog_internal.h +++ b/src/include/access/xlog_internal.h @@ -31,7 +31,7 @@ /* * Each page of XLOG file has a header like this: */ -#define XLOG_PAGE_MAGIC 0xD11E /* can be used as WAL version indicator */ +#define XLOG_PAGE_MAGIC 0xD11F /* can be used as WAL version indicator */ typedef struct XLogPageHeaderData { diff --git a/src/include/storage/procarray.h b/src/include/storage/procarray.h index abdf021e66e..377b3060b9f 100644 --- a/src/include/storage/procarray.h +++ b/src/include/storage/procarray.h @@ -49,7 +49,7 @@ extern bool ProcArrayInstallImportedXmin(TransactionId xmin, VirtualTransactionId *sourcevxid); extern bool ProcArrayInstallRestoredXmin(TransactionId xmin, PGPROC *proc); -extern RunningTransactions GetRunningTransactionData(void); +extern RunningTransactions GetRunningTransactionData(Oid dbid); extern bool TransactionIdIsInProgress(TransactionId xid); extern TransactionId GetOldestNonRemovableTransactionId(Relation rel); diff --git a/src/include/storage/standby.h b/src/include/storage/standby.h index 6a314c693cd..8715c08e94f 100644 --- a/src/include/storage/standby.h +++ b/src/include/storage/standby.h @@ -126,6 +126,7 @@ typedef enum typedef struct RunningTransactionsData { + Oid dbid; /* only track xacts in this database */ int xcnt; /* # of xact ids in xids[] */ int subxcnt; /* # of subxact ids in xids[] */ subxids_array_status subxid_status; @@ -143,7 +144,7 @@ typedef RunningTransactionsData *RunningTransactions; extern void LogAccessExclusiveLock(Oid dbOid, Oid relOid); extern void LogAccessExclusiveLockPrepare(void); -extern XLogRecPtr LogStandbySnapshot(void); +extern XLogRecPtr LogStandbySnapshot(Oid dbid); extern void LogStandbyInvalidations(int nmsgs, SharedInvalidationMessage *msgs, bool relcacheInitFileInval); diff --git a/src/include/storage/standbydefs.h b/src/include/storage/standbydefs.h index 231d251fd51..e75b7078766 100644 --- a/src/include/storage/standbydefs.h +++ b/src/include/storage/standbydefs.h @@ -46,6 +46,7 @@ typedef struct xl_standby_locks */ typedef struct xl_running_xacts { + Oid dbid; /* only track xacts in this database */ int xcnt; /* # of xact ids in xids[] */ int subxcnt; /* # of subxact ids in xids[] */ bool subxid_overflow; /* snapshot overflowed, subxids missing */ -- 2.47.3 --r2slln3zpmilwu22 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v51-0009-Reserve-replication-slots-specifically-for-REPAC.patch" From ac1a9746b8e1e1fb2255f8eca0ce6d21caad1ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Wed, 1 Apr 2026 19:54:14 +0200 Subject: [PATCH v51 09/10] Reserve replication slots specifically for REPACK This allows REPACK to not interfere with other operations that use replication slots. This eases configurability. --- doc/src/sgml/config.sgml | 16 ++++ doc/src/sgml/ref/repack.sgml | 6 +- src/backend/commands/repack.c | 3 +- src/backend/commands/repack_worker.c | 4 +- src/backend/replication/logical/launcher.c | 2 +- src/backend/replication/logical/slotsync.c | 5 +- src/backend/replication/slot.c | 80 +++++++++++-------- src/backend/replication/slotfuncs.c | 8 +- src/backend/replication/walsender.c | 4 +- src/backend/utils/misc/guc_parameters.dat | 8 ++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/replication/slot.h | 3 +- 12 files changed, 93 insertions(+), 48 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 422ba304982..a67dd6b9eb1 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -4638,6 +4638,22 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows + + max_repack_replication_slots (integer) + + max_repack_replication_slots configuration parameter + + + + + Specifies the maximum number of replication slots for use of + the REPACK command. The default is 5. + This parameter can only be set at server start. + + + + + max_replication_slots (integer) diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml index bec18c44cc8..1424446ba7c 100644 --- a/doc/src/sgml/ref/repack.sgml +++ b/doc/src/sgml/ref/repack.sgml @@ -293,9 +293,9 @@ REPACK [ ( option [, ...] ) ] USING - The max_replication_slots - configuration parameter does not allow for creation of an additional - replication slot. + The max_repack_replication_slots + configuration parameter does not allow for the creation of an + additional replication slot. diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index d4c1f0e7652..d932d526235 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3381,7 +3381,8 @@ start_repack_decoding_worker(Oid relid) ereport(ERROR, errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("out of background worker slots"), - errhint("You might need to increase \"%s\".", "max_worker_processes")); + /* FIXME rename to max_repack_processes? */ + errhint("You might need to increase \"%s\".", "max_repack_replication_slots")); decoding_worker->seg = seg; decoding_worker->error_mqh = mqh; diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index ff34e246469..610592a05b0 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -233,8 +233,8 @@ repack_setup_logical_decoding(Oid relid) * RS_TEMPORARY so that the slot gets cleaned up on ERROR. */ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid); - ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false, - false); + ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, true, + false, false); EnsureLogicalDecodingEnabled(); diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c index 09964198550..d83125afd0d 100644 --- a/src/backend/replication/logical/launcher.c +++ b/src/backend/replication/logical/launcher.c @@ -1575,7 +1575,7 @@ CreateConflictDetectionSlot(void) errmsg("creating replication conflict detection slot")); ReplicationSlotCreate(CONFLICT_DETECTION_SLOT, false, RS_PERSISTENT, false, - false, false); + false, false, false); init_conflict_slot_xmin(); } diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c index e75db69e3f6..28c89c5e10d 100644 --- a/src/backend/replication/logical/slotsync.c +++ b/src/backend/replication/logical/slotsync.c @@ -425,7 +425,7 @@ get_local_synced_slots(void) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -814,6 +814,7 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid, */ ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY, remote_slot->two_phase, + false, remote_slot->failover, true); @@ -1691,7 +1692,7 @@ update_synced_slots_inactive_since(void) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c index d553bd5dbff..2c6c6773ad2 100644 --- a/src/backend/replication/slot.c +++ b/src/backend/replication/slot.c @@ -152,6 +152,8 @@ ReplicationSlot *MyReplicationSlot = NULL; /* GUC variables */ int max_replication_slots = 10; /* the maximum number of replication * slots */ +int max_repack_replication_slots = 5; /* the maximum number of slots + * for REPACK */ /* * Invalidate replication slots that have remained idle longer than this @@ -189,14 +191,15 @@ static void SaveSlotToPath(ReplicationSlot *slot, const char *dir, int elevel); Size ReplicationSlotsShmemSize(void) { + int totalslots = max_replication_slots + max_repack_replication_slots; Size size = 0; - if (max_replication_slots == 0) + if (totalslots == 0) return size; size = offsetof(ReplicationSlotCtlData, replication_slots); size = add_size(size, - mul_size(max_replication_slots, sizeof(ReplicationSlot))); + mul_size(totalslots, sizeof(ReplicationSlot))); return size; } @@ -209,7 +212,7 @@ ReplicationSlotsShmemInit(void) { bool found; - if (max_replication_slots == 0) + if (max_replication_slots + max_repack_replication_slots == 0) return; ReplicationSlotCtl = (ReplicationSlotCtlData *) @@ -223,7 +226,7 @@ ReplicationSlotsShmemInit(void) /* First time through, so initialize */ MemSet(ReplicationSlotCtl, 0, ReplicationSlotsShmemSize()); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[i]; @@ -373,6 +376,7 @@ IsSlotForConflictCheck(const char *name) * db_specific: logical decoding is db specific; if the slot is going to * be used for that pass true, otherwise false. * two_phase: If enabled, allows decoding of prepared transactions. + * repack: If true, use a slot from the pool for REPACK. * failover: If enabled, allows the slot to be synced to standbys so * that logical replication can be resumed after failover. * synced: True if the slot is synchronized from the primary server. @@ -380,10 +384,11 @@ IsSlotForConflictCheck(const char *name) void ReplicationSlotCreate(const char *name, bool db_specific, ReplicationSlotPersistency persistency, - bool two_phase, bool failover, bool synced) + bool two_phase, bool repack, bool failover, bool synced) { ReplicationSlot *slot = NULL; - int i; + int startpoint, + endpoint; Assert(MyReplicationSlot == NULL); @@ -432,12 +437,16 @@ ReplicationSlotCreate(const char *name, bool db_specific, LWLockAcquire(ReplicationSlotAllocationLock, LW_EXCLUSIVE); /* - * Check for name collision, and identify an allocatable slot. We need to - * hold ReplicationSlotControlLock in shared mode for this, so that nobody - * else can change the in_use flags while we're looking at them. + * Check for name collision (across the whole array), and identify an + * allocatable slot (in the array slice specific to our current use case: + * either general, or REPACK only). We need to hold + * ReplicationSlotControlLock in shared mode for this, so that nobody else + * can change the in_use flags while we're looking at them. */ LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + startpoint = !repack ? 0 : max_replication_slots; + endpoint = max_replication_slots + (repack ? max_repack_replication_slots : 0); + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -445,7 +454,9 @@ ReplicationSlotCreate(const char *name, bool db_specific, ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("replication slot \"%s\" already exists", name))); - if (!s->in_use && slot == NULL) + + if (i >= startpoint && i < endpoint && + !s->in_use && slot == NULL) slot = s; } LWLockRelease(ReplicationSlotControlLock); @@ -455,7 +466,8 @@ ReplicationSlotCreate(const char *name, bool db_specific, ereport(ERROR, (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("all replication slots are in use"), - errhint("Free one or increase \"max_replication_slots\"."))); + errhint("Free one or increase \"%s\".", + repack ? "max_repack_replication_slots" : "max_replication_slots"))); /* * Since this slot is not in use, nobody should be looking at any part of @@ -548,7 +560,7 @@ SearchNamedReplicationSlot(const char *name, bool need_lock) if (need_lock) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -576,7 +588,8 @@ int ReplicationSlotIndex(ReplicationSlot *slot) { Assert(slot >= ReplicationSlotCtl->replication_slots && - slot < ReplicationSlotCtl->replication_slots + max_replication_slots); + slot < ReplicationSlotCtl->replication_slots + + (max_replication_slots + max_repack_replication_slots)); return slot - ReplicationSlotCtl->replication_slots; } @@ -870,7 +883,7 @@ ReplicationSlotCleanup(bool synced_only) restart: found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -1252,7 +1265,7 @@ ReplicationSlotsComputeRequiredXmin(bool already_locked) if (!already_locked) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; TransactionId effective_xmin; @@ -1307,7 +1320,7 @@ ReplicationSlotsComputeRequiredLSN(void) Assert(ReplicationSlotCtl != NULL); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; XLogRecPtr restart_lsn; @@ -1374,12 +1387,12 @@ ReplicationSlotsComputeLogicalRestartLSN(void) XLogRecPtr result = InvalidXLogRecPtr; int i; - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return InvalidXLogRecPtr; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s; XLogRecPtr restart_lsn; @@ -1454,11 +1467,11 @@ ReplicationSlotsCountDBSlots(Oid dboid, int *nslots, int *nactive) *nslots = *nactive = 0; - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s; @@ -1515,13 +1528,13 @@ ReplicationSlotsDropDBSlots(Oid dboid) bool found_valid_logicalslot; bool dropped = false; - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return; restart: found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s; char *slotname; @@ -1618,11 +1631,11 @@ CheckLogicalSlotExists(void) { bool found = false; - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s; bool invalidated; @@ -1663,10 +1676,12 @@ CheckSlotRequirements(void) * needs the same check. */ - if (max_replication_slots == 0) + /* XXX we should be able to check exactly which type of slot we need */ + if (max_replication_slots + max_repack_replication_slots == 0) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("replication slots can only be used if \"max_replication_slots\" > 0"))); + errmsg("replication slots can only be used if \"%s\" > 0 or \"%s\" > 0", + "max_replication_slots", "max_repack_replication_slots"))); if (wal_level < WAL_LEVEL_REPLICA) ereport(ERROR, @@ -2224,7 +2239,7 @@ InvalidateObsoleteReplicationSlots(uint32 possible_causes, Assert(!(possible_causes & RS_INVAL_WAL_REMOVED) || oldestSegno > 0); Assert(possible_causes != RS_INVAL_NONE); - if (max_replication_slots == 0) + if (max_replication_slots == 0 && max_repack_replication_slots == 0) return invalidated; XLogSegNoOffsetToRecPtr(oldestSegno, 0, wal_segment_size, oldestLSN); @@ -2232,7 +2247,7 @@ InvalidateObsoleteReplicationSlots(uint32 possible_causes, restart: found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; bool released_lock = false; @@ -2337,7 +2352,7 @@ CheckPointReplicationSlots(bool is_shutdown) */ LWLockAcquire(ReplicationSlotAllocationLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; char path[MAXPGPATH]; @@ -2438,7 +2453,7 @@ StartupReplicationSlots(void) FreeDir(replication_dir); /* currently no slots exist, we're done. */ - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return; /* Now that we have recovered all the data, compute replication xmin */ @@ -2868,7 +2883,7 @@ RestoreSlotFromDisk(const char *name) errhint("Change \"wal_level\" to be \"replica\" or higher."))); /* nothing can be active yet, don't lock anything */ - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *slot; @@ -2910,6 +2925,7 @@ RestoreSlotFromDisk(const char *name) break; } + /* XXX might be misleading if the slots previously in use were REPACK. */ if (!restored) ereport(FATAL, (errmsg("too many replication slots active before shutdown"), diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c index 9f5e4f998fe..78dd3c4ea66 100644 --- a/src/backend/replication/slotfuncs.c +++ b/src/backend/replication/slotfuncs.c @@ -53,7 +53,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve, /* acquire replication slot, this will check for conflicting names */ ReplicationSlotCreate(name, false, temporary ? RS_TEMPORARY : RS_PERSISTENT, false, - false, false); + false, false, false); if (immediately_reserve) { @@ -146,7 +146,7 @@ create_logical_replication_slot(char *name, char *plugin, */ ReplicationSlotCreate(name, true, temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase, - failover, false); + false, failover, false); /* * Ensure the logical decoding is enabled before initializing the logical @@ -270,7 +270,7 @@ pg_get_replication_slots(PG_FUNCTION_ARGS) currlsn = GetXLogWriteRecPtr(); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (slotno = 0; slotno < max_replication_slots; slotno++) + for (slotno = 0; slotno < max_replication_slots + max_repack_replication_slots; slotno++) { ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[slotno]; ReplicationSlot slot_contents; @@ -665,7 +665,7 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot) * managed to create the new slot, we advance the new slot's restart_lsn * to the source slot's updated restart_lsn the second time we lock it. */ - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index 2bb3f34dc6d..75ef3419a15 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -1220,7 +1220,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) { ReplicationSlotCreate(cmd->slotname, false, cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT, - false, false, false); + false, false, false, false); if (reserve_wal) { @@ -1251,7 +1251,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) */ ReplicationSlotCreate(cmd->slotname, true, cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL, - two_phase, failover, false); + two_phase, false, failover, false); /* * Do options check early so that we can bail before calling the diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index fc0900efe5f..857be159f5c 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -2070,6 +2070,14 @@ max => 'MAX_BACKENDS', }, +{ name => 'max_repack_replication_slots', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SENDING', + short_desc => 'Sets the maximum number of replication slots for use by REPACK.', + variable => 'max_repack_replication_slots', + boot_val => '5', + min => '0', + max => 'MAX_BACKENDS', +}, + /* see max_wal_senders */ { name => 'max_replication_slots', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SENDING', short_desc => 'Sets the maximum number of simultaneously defined replication slots.', diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index c8194c27aa7..9c3c5d16361 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -348,6 +348,8 @@ # (change requires restart) #max_replication_slots = 10 # max number of replication slots # (change requires restart) +#max_repack_replication_slots = 5 # max number of replication slots for REPACK + # (change requires restart) #wal_keep_size = 0 # in megabytes; 0 disables #max_slot_wal_keep_size = -1 # in megabytes; -1 disables #idle_replication_slot_timeout = 0 # in seconds; 0 disables diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h index 4b4709f6e2c..c316a01a807 100644 --- a/src/include/replication/slot.h +++ b/src/include/replication/slot.h @@ -324,6 +324,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot; /* GUCs */ extern PGDLLIMPORT int max_replication_slots; +extern PGDLLIMPORT int max_repack_replication_slots; extern PGDLLIMPORT char *synchronized_standby_slots; extern PGDLLIMPORT int idle_replication_slot_timeout_secs; @@ -334,7 +335,7 @@ extern void ReplicationSlotsShmemInit(void); /* management of individual slots */ extern void ReplicationSlotCreate(const char *name, bool db_specific, ReplicationSlotPersistency persistency, - bool two_phase, bool failover, + bool two_phase, bool repack, bool failover, bool synced); extern void ReplicationSlotPersist(void); extern void ReplicationSlotDrop(const char *name, bool nowait); -- 2.47.3 --r2slln3zpmilwu22 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v51-0010-CheckLogicalDecodingRequirements-be-specific-abo.patch" From 0135cc5c8921d2b3eeadc2575bb9e13ad018b39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= Date: Fri, 3 Apr 2026 21:01:23 +0200 Subject: [PATCH v51 10/10] CheckLogicalDecodingRequirements: be specific about which GUC is limiting --- src/backend/commands/repack_worker.c | 3 ++- src/backend/replication/logical/logical.c | 8 +++++--- src/backend/replication/logical/logicalfuncs.c | 2 +- src/backend/replication/slot.c | 18 ++++++++++++------ src/backend/replication/slotfuncs.c | 11 ++++++----- src/backend/replication/walsender.c | 5 +++-- src/include/replication/logical.h | 3 ++- src/include/replication/slot.h | 2 +- 8 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index 610592a05b0..ca827223845 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -224,7 +224,7 @@ repack_setup_logical_decoding(Oid relid) * Make sure we can use logical decoding. */ CheckSlotPermissions(); - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(true); /* * A single backend should not execute multiple REPACK commands at a time, @@ -252,6 +252,7 @@ repack_setup_logical_decoding(Oid relid) ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, + true, InvalidXLogRecPtr, XL_ROUTINE(.page_read = read_local_xlog_page, .segment_open = wal_segment_open, diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c index f20a0fe70ad..a08aece5731 100644 --- a/src/backend/replication/logical/logical.c +++ b/src/backend/replication/logical/logical.c @@ -108,9 +108,9 @@ static void LoadOutputPlugin(OutputPluginCallbacks *callbacks, const char *plugi * decoding. */ void -CheckLogicalDecodingRequirements(void) +CheckLogicalDecodingRequirements(bool repack) { - CheckSlotRequirements(); + CheckSlotRequirements(repack); /* * NB: Adding a new requirement likely means that RestoreSlotFromDisk() @@ -305,6 +305,7 @@ StartupDecodingContext(List *output_plugin_options, * output_plugin_options -- contains options passed to the output plugin * need_full_snapshot -- if true, must obtain a snapshot able to read all * tables; if false, one that can read only catalogs is acceptable. + * for_repack -- if true, we're going to be decoding for REPACK. * restart_lsn -- if given as invalid, it's this routine's responsibility to * mark WAL as reserved by setting a convenient restart_lsn for the slot. * Otherwise, we set for decoding to start from the given LSN without @@ -325,6 +326,7 @@ LogicalDecodingContext * CreateInitDecodingContext(const char *plugin, List *output_plugin_options, bool need_full_snapshot, + bool for_repack, XLogRecPtr restart_lsn, XLogReaderRoutine *xl_routine, LogicalOutputPluginWriterPrepareWrite prepare_write, @@ -341,7 +343,7 @@ CreateInitDecodingContext(const char *plugin, * On a standby, this check is also required while creating the slot. * Check the comments in the function. */ - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(for_repack); /* shorter lines... */ slot = MyReplicationSlot; diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c index 9760818941d..512013b0ef0 100644 --- a/src/backend/replication/logical/logicalfuncs.c +++ b/src/backend/replication/logical/logicalfuncs.c @@ -115,7 +115,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin CheckSlotPermissions(); - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); if (PG_ARGISNULL(0)) ereport(ERROR, diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c index 2c6c6773ad2..13004ed547a 100644 --- a/src/backend/replication/slot.c +++ b/src/backend/replication/slot.c @@ -1669,19 +1669,25 @@ CheckLogicalSlotExists(void) * slots. */ void -CheckSlotRequirements(void) +CheckSlotRequirements(bool repack) { + int limit; + /* * NB: Adding a new requirement likely means that RestoreSlotFromDisk() * needs the same check. */ - /* XXX we should be able to check exactly which type of slot we need */ - if (max_replication_slots + max_repack_replication_slots == 0) + if (repack) + limit = max_repack_replication_slots; + else + limit = max_replication_slots; + + if (limit == 0) ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("replication slots can only be used if \"%s\" > 0 or \"%s\" > 0", - "max_replication_slots", "max_repack_replication_slots"))); + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("replication slots can only be used if \"%s\" > 0", + repack ? "max_repack_replication_slots" : "max_replication_slots")); if (wal_level < WAL_LEVEL_REPLICA) ereport(ERROR, diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c index 78dd3c4ea66..16fbd383735 100644 --- a/src/backend/replication/slotfuncs.c +++ b/src/backend/replication/slotfuncs.c @@ -90,7 +90,7 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS) CheckSlotPermissions(); - CheckSlotRequirements(); + CheckSlotRequirements(false); create_physical_replication_slot(NameStr(*name), immediately_reserve, @@ -164,6 +164,7 @@ create_logical_replication_slot(char *name, char *plugin, */ ctx = CreateInitDecodingContext(plugin, NIL, false, /* just catalogs is OK */ + false, /* not repack */ restart_lsn, XL_ROUTINE(.page_read = read_local_xlog_page, .segment_open = wal_segment_open, @@ -203,7 +204,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS) CheckSlotPermissions(); - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); create_logical_replication_slot(NameStr(*name), NameStr(*plugin), @@ -240,7 +241,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS) CheckSlotPermissions(); - CheckSlotRequirements(); + CheckSlotRequirements(false); ReplicationSlotDrop(NameStr(*name), true); @@ -648,9 +649,9 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot) CheckSlotPermissions(); if (logical_slot) - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); else - CheckSlotRequirements(); + CheckSlotRequirements(false); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index 75ef3419a15..9d7d675fa96 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -1240,7 +1240,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) Assert(cmd->kind == REPLICATION_KIND_LOGICAL); - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); /* * Initially create persistent slot as ephemeral - that allows us to @@ -1309,6 +1309,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) Assert(IsLogicalDecodingEnabled()); ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot, + false, InvalidXLogRecPtr, XL_ROUTINE(.page_read = logical_read_xlog_page, .segment_open = WalSndSegmentOpen, @@ -1466,7 +1467,7 @@ StartLogicalReplication(StartReplicationCmd *cmd) QueryCompletion qc; /* make sure that our requirements are still fulfilled */ - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); Assert(!MyReplicationSlot); diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h index bc9d4ece672..bc075b16741 100644 --- a/src/include/replication/logical.h +++ b/src/include/replication/logical.h @@ -115,11 +115,12 @@ typedef struct LogicalDecodingContext } LogicalDecodingContext; -extern void CheckLogicalDecodingRequirements(void); +extern void CheckLogicalDecodingRequirements(bool repack); extern LogicalDecodingContext *CreateInitDecodingContext(const char *plugin, List *output_plugin_options, bool need_full_snapshot, + bool for_repack, XLogRecPtr restart_lsn, XLogReaderRoutine *xl_routine, LogicalOutputPluginWriterPrepareWrite prepare_write, diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h index c316a01a807..489af7d8d6c 100644 --- a/src/include/replication/slot.h +++ b/src/include/replication/slot.h @@ -378,7 +378,7 @@ extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname extern void StartupReplicationSlots(void); extern void CheckPointReplicationSlots(bool is_shutdown); -extern void CheckSlotRequirements(void); +extern void CheckSlotRequirements(bool repack); extern void CheckSlotPermissions(void); extern ReplicationSlotInvalidationCause GetSlotInvalidationCause(const char *cause_name); -- 2.47.3 --r2slln3zpmilwu22--