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.94.2) (envelope-from ) id 1tzbiV-004BfU-TY for pgsql-hackers@arkaria.postgresql.org; Tue, 01 Apr 2025 13:31:48 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.94.2) (envelope-from ) id 1tzbiU-002KgE-JG for pgsql-hackers@arkaria.postgresql.org; Tue, 01 Apr 2025 13:31:46 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1tzbiT-002Kg6-Tc for pgsql-hackers@lists.postgresql.org; Tue, 01 Apr 2025 13:31:46 +0000 Received: from mail-wm1-x331.google.com ([2a00:1450:4864:20::331]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.96) (envelope-from ) id 1tzbiN-002l3U-0r for pgsql-hackers@postgresql.org; Tue, 01 Apr 2025 13:31:45 +0000 Received: by mail-wm1-x331.google.com with SMTP id 5b1f17b1804b1-43d2d952eb1so38808715e9.1 for ; Tue, 01 Apr 2025 06:31:38 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cybertec.at; s=google; t=1743514298; x=1744119098; darn=postgresql.org; h=message-id:date:mime-version:comments:references:in-reply-to :subject:cc:to:from:from:to:cc:subject:date:message-id:reply-to; bh=TzH9a5xhrSiBw/a+mJ7Uv7hSWSdtIoO006w3hZFrNm0=; b=C7q36UShwrP1MgAvUZ/k1twThmnxc9C5PD0STli7smsnZT4woclfqhUHTlPHsgGM+p AardLg6gFXFJKDj1RObw7c7+rbeYhDy201dEz86rwhMPBRmSaDUCgrtRWdMiZww4xPqc kj5jvlovvYC+alXNCtsPTd6dnxVgFtsit0rkfp2KOwWhtKa0yT5OqQwPrhxdqyvoRsZ9 IzygoFM44LdqqeZT9DrhAAe1LD85jgniOjoEEVvEfy7nFIkaLVhLPsdc5uWkohh1mykn aPmITL5ENv3D6AkBKL8BKGI4DY63Q71KCjM/7TI0HC/S2LrAQqQB0q9aTcm04xeONzkU S7mQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1743514298; x=1744119098; h=message-id:date:mime-version:comments:references:in-reply-to :subject:cc:to:from:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=TzH9a5xhrSiBw/a+mJ7Uv7hSWSdtIoO006w3hZFrNm0=; b=SAzEBuPgrLjwVcTylmfiefP4poeJlcKaHg8CYGOOGhBpmSVVHFV85gV5lqWUJJZgAQ /qJCjX98vI5+ckCupmxOKsKW8SJHDSfsC1HXEOqoQu6CqdlY1OGrNOM8QBdXcQcxCM5b zb+Ag4EB6M1464ihjmIhy1CY1GcsqsRNYy+tbTTBEirGBq/Yq7WuniVo61uV25PkFLdv 3NQSc+UA2Eyc3prK7alwUx0wpPocoLAjLqV1hZWUBvuIvTc1tJ+RHyNCKDMVoM4tayYv yvi2VsUlfdG2hzwtNm7hNhxdY4Y0a/gYITSqvmokXgXy1NI8eUQEB13LDUcuwc3sTSYx TTDQ== X-Forwarded-Encrypted: i=1; AJvYcCVau2WOKy41lnANiTUFDEtL6dgqUtV6rnpmV/tBavwT9HHdy22vOdkkcbwbFMo7h67TehBwPm8ynNveY5mm@postgresql.org X-Gm-Message-State: AOJu0Yz8xW/CnvM7KTbx9RHGXov7IKy1hyVLkHIvm7SgpmzpX9Iiumx8 HwSYlM4v9Yn3zKHNs3oU9q8lvMwFIF9N5AqalrzvIUTHjlxo4gSL4OgvFv7QZTY= X-Gm-Gg: ASbGncuKuHDdmn3UB/Kms68XeMv7CU3sQrwtj/6i3q896ya8ok2gMdahMyjypiPVpDp lKly/dVk2Et8EhEtEfxRCTKmqv7PJjGvt03LS8dwyV4f3vTE9wHnBGiXfbpdfNMoOvMF/JYAg/b y5fQAn1EVlP+rE7Iwt2u4FekV6raj7E4luN1Iz3vgk1cRZ1F+GV2d7FtzdSHt9VjQlwSXrnGjWi IE9yJEgGWcphRTdyIO2pK89N+M/bWHf0NRTpJyiLXgdn3/ZyfIxtsCbXiocOD5mfAwvAH2IYi9d tRg/8fBd6HLfxCN88gunp3toccRrLv7qiskm1CRNEXkvyLXJjo8ejj6XELj+GIh7ilA= X-Google-Smtp-Source: AGHT+IGmsjD51ucAbe0PdBpXBIau+xdwafH1AMfCBT+SuByoyOailn6rbk5rv0rSCBG6J3lO9c2AUA== X-Received: by 2002:a05:600c:34ce:b0:43c:e7ae:4bcf with SMTP id 5b1f17b1804b1-43dabe235b5mr139184235e9.0.1743514293252; Tue, 01 Apr 2025 06:31:33 -0700 (PDT) Received: from localhost (109-81-174-36.rct.o2.cz. [109.81.174.36]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-43d8ff042bcsm155532285e9.28.2025.04.01.06.31.32 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 01 Apr 2025 06:31:32 -0700 (PDT) From: Antonin Houska To: Alvaro Herrera Cc: Marcos Pegoraro , Michael Banck , Junwang Zhao , Kirill Reshke , Pavel Stehule , Michael Paquier , PostgreSQL Hackers Subject: Re: why there is not VACUUM FULL CONCURRENTLY? In-reply-to: <104580.1743431185@localhost> References: <202503251432.o6obtgywjow5@alvherre.pgsql> <6059.1743060030@localhost> <104580.1743431185@localhost> Comments: In-reply-to Antonin Houska message dated "Mon, 31 Mar 2025 16:26:25 +0200." X-Mailer: MH-E 8.6+git; nmh 1.8; GNU Emacs 28.3 MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Date: Tue, 01 Apr 2025 15:31:31 +0200 Message-ID: <178741.1743514291@localhost> List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --=-=-= Content-Type: text/plain Antonin Houska wrote: > Antonin Houska wrote: > > > Alvaro Herrera wrote: > > > > > On 2025-Mar-22, Antonin Houska wrote: > > > > > > > Alvaro Herrera wrote: > > > > > > > > > I rebased this patch series; here's v09. No substantive changes from v08. > > > > > I made sure the tree still compiles after each commit. > > > > > > I rebased again, fixing a compiler warning reported by CI and applying > > > pgindent to each individual patch. I'm slowly starting to become more > > > familiar with the whole of this new code. > > > > I'm trying to reflect Robert's suggestions about locking [1]. The next version > > should be a bit simpler, so maybe wait for it before you continue studying the > > code. > > This is it. One more version, hopefully to make cfbot happy (I missed the bug because I did not set the RELCACHE_FORCE_RELEASE macro in my environment.) Besides that, it occurred to me that 0005 ("Preserve visibility information of the concurrent data changes.") will probably introduce significant overhead. The problem is that the table we're repacking is treated like a catalog, for reorderbuffer.c to generate snapshots that we need to replay UPDATE / DELETE commands on the new table. contrib/test_decoding can be used to demonstrate the difference between ordinary and catalog tables: CREATE TABLE t(i int); SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding'); INSERT INTO t SELECT n FROM generate_series(1, 1000000) g(n); DELETE FROM t; EXPLAIN ANALYZE SELECT * FROM pg_logical_slot_get_binary_changes('test_slot', null, null); ... Execution Time: 3521.190 ms ALTER TABLE t SET (user_catalog_table = true); INSERT INTO t SELECT n FROM generate_series(1, 1000000) g(n); DELETE FROM t; EXPLAIN ANALYZE SELECT * FROM pg_logical_slot_get_binary_changes('test_slot', null, null); ... Execution Time: 6561.634 ms I wanted to avoid the "MVCC unsafety" [1], so that both REPACK and REPACK CONCURRENTLY both work "cleanly". We can try to optimize the logical decoding for REPACK CONCURRENTLY, or implement 0005 in a different way, but not sure how much effort that would require. Or implement REPACK CONCURRENTLY as MVCC-unsafe for now? (The pg_squeeze extension also is not MVCC-safe.) [1] https://www.postgresql.org/docs/17/mvcc-caveats.html -- Antonin Houska Web: https://www.cybertec-postgresql.com --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v12-0001-Add-REPACK-command.patch From 2dbf5df9f3a2c7839459865f2e2fc83a1859ce23 Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Tue, 1 Apr 2025 13:48:57 +0200 Subject: [PATCH 1/9] Add REPACK command. The existing CLUSTER command as well as VACUUM with the FULL option both reclaim unused space by rewriting table. Now that we want to enhance this functionality (in particular, by adding a new option CONCURRENTLY), we should enhance both commands because they are both implemented by the same function (cluster.c:cluster_rel). However, adding the same option to two different commands is not very user-friendly. Therefore it was decided to create a new command and to declare both CLUSTER command and the FULL option of VACUUM deprecated. Future enhancements to this rewriting code will only affect the new command. Like CLUSTER, the REPACK command reorders the table according to the specified index. Unlike CLUSTER, REPACK does not require the index: if only table is specified, the command acts as VACUUM FULL. As we don't want to remove CLUSTER and VACUUM FULL yet, there are three callers of the cluster_rel() function now: REPACK, CLUSTER and VACUUM FULL. When we need to distinguish who is calling this function (mostly for logging, but also for progress reporting), we can no longer use the OID of the clustering index: both REPACK and VACUUM FULL can pass InvalidOid. Therefore, this patch introduces a new enumeration type ClusterCommand, and adds an argument of this type to the cluster_rel() function and to all the functions that need to distinguish the caller. Like CLUSTER and VACUUM FULL, the REPACK COMMAND without arguments processes all the tables on which the current user has the MAINTAIN privilege. A new view pg_stat_progress_repack view is added to monitor the progress of REPACK. Currently it displays the same information as pg_stat_progress_cluster (except that column names might differ), but it'll also display the status of the REPACK CONCURRENTLY command in the future, so the view definitions will eventually diverge. Regarding user documentation, the patch moves the information on clustering from cluster.sgml to the new file repack.sgml. cluster.sgml now contains a link that points to the related section of repack.sgml. A note on deprecation and a link to repack.sgml are added to both cluster.sgml and vacuum.sgml. --- doc/src/sgml/monitoring.sgml | 230 +++++++++++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/cluster.sgml | 79 +--- doc/src/sgml/ref/repack.sgml | 254 ++++++++++++ doc/src/sgml/ref/vacuum.sgml | 8 + doc/src/sgml/reference.sgml | 1 + src/backend/access/heap/heapam_handler.c | 32 +- src/backend/catalog/index.c | 2 +- src/backend/catalog/system_views.sql | 27 ++ src/backend/commands/cluster.c | 496 +++++++++++++++++------ src/backend/commands/tablecmds.c | 3 +- src/backend/commands/vacuum.c | 3 +- src/backend/parser/gram.y | 63 ++- src/backend/tcop/utility.c | 9 + src/backend/utils/adt/pgstatfuncs.c | 2 + src/bin/psql/tab-complete.in.c | 31 +- src/include/commands/cluster.h | 22 +- src/include/commands/progress.h | 60 ++- src/include/nodes/parsenodes.h | 13 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + src/include/utils/backend_progress.h | 1 + src/test/regress/expected/cluster.out | 180 ++++++++ src/test/regress/expected/rules.out | 27 ++ src/test/regress/sql/cluster.sql | 73 ++++ src/tools/pgindent/typedefs.list | 2 + 26 files changed, 1385 insertions(+), 236 deletions(-) create mode 100644 doc/src/sgml/ref/repack.sgml diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index a6d67d2fbaa..0a6229c391a 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -400,6 +400,14 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser + + pg_stat_progress_repackpg_stat_progress_repack + One row for each backend running + REPACK, showing current progress. See + . + + + pg_stat_progress_basebackuppg_stat_progress_basebackup One row for each WAL sender process streaming a base backup, @@ -5940,6 +5948,228 @@ FROM pg_stat_get_backend_idset() AS backendid; + + REPACK Progress Reporting + + + pg_stat_progress_repack + + + + Whenever REPACK is running, + the pg_stat_progress_repack view will contain a + row for each backend that is currently running the command. The tables + below describe the information that will be reported and provide + information about how to interpret it. + + + + <structname>pg_stat_progress_repack</structname> View + + + + + Column Type + + + Description + + + + + + + + pid integer + + + Process ID of backend. + + + + + + datid oid + + + OID of the database to which this backend is connected. + + + + + + datname name + + + Name of the database to which this backend is connected. + + + + + + relid oid + + + OID of the table being repacked. + + + + + + command text + + + The command that is running. Currently, the only value + is REPACK. + + + + + + phase text + + + Current processing phase. See . + + + + + + repack_index_relid oid + + + If the table is being scanned using an index, this is the OID of the + index being used; otherwise, it is zero. + + + + + + heap_tuples_scanned bigint + + + Number of heap tuples scanned. + This counter only advances when the phase is + seq scanning heap, + index scanning heap + or writing new heap. + + + + + + heap_tuples_written bigint + + + Number of heap tuples written. + This counter only advances when the phase is + seq scanning heap, + index scanning heap + or writing new heap. + + + + + + heap_blks_total bigint + + + Total number of heap blocks in the table. This number is reported + as of the beginning of seq scanning heap. + + + + + + heap_blks_scanned bigint + + + Number of heap blocks scanned. This counter only advances when the + phase is seq scanning heap. + + + + + + index_rebuild_count bigint + + + Number of indexes rebuilt. This counter only advances when the phase + is rebuilding index. + + + + +
+ + + REPACK Phases + + + + + + Phase + Description + + + + + + initializing + + The command is preparing to begin scanning the heap. This phase is + expected to be very brief. + + + + seq scanning heap + + The command is currently scanning the table using a sequential scan. + + + + index scanning heap + + REPACK is currently scanning the table using an index scan. + + + + sorting tuples + + REPACK is currently sorting tuples. + + + + writing new heap + + REPACK is currently writing the new heap. + + + + swapping relation files + + The command is currently swapping newly-built files into place. + + + + rebuilding index + + The command is currently rebuilding an index. + + + + performing final cleanup + + The command is performing final cleanup. When this phase is + completed, REPACK will end. + + + + +
+
+ COPY Progress Reporting diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..c0ef654fcb4 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -167,6 +167,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/cluster.sgml b/doc/src/sgml/ref/cluster.sgml index 8811f169ea0..54bb2362c84 100644 --- a/doc/src/sgml/ref/cluster.sgml +++ b/doc/src/sgml/ref/cluster.sgml @@ -42,17 +42,23 @@ CLUSTER [ ( option [, ...] ) ] [ table_name. - - When a table is clustered, it is physically reordered - based on the index information. Clustering is a one-time operation: - when the table is subsequently updated, the changes are - not clustered. That is, no attempt is made to store new or - updated rows according to their index order. (If one wishes, one can - periodically recluster by issuing the command again. Also, setting - the table's fillfactor storage parameter to less than - 100% can aid in preserving cluster ordering during updates, since updated - rows are kept on the same page if enough space is available there.) - + + + The CLUSTER command is deprecated in favor of + . + + + + + + explain how clustering + works, whether it is initiated by CLUSTER or + by REPACK. The notable difference between the two is + that REPACK does not remember the index used last + time. Thus if you don't specify an index, REPACK + rewrites the table but does not try to cluster it. + + When a table is clustered, PostgreSQL @@ -136,63 +142,12 @@ CLUSTER [ ( option [, ...] ) ] [ - - In cases where you are accessing single rows randomly - within a table, the actual order of the data in the - table is unimportant. However, if you tend to access some - data more than others, and there is an index that groups - them together, you will benefit from using CLUSTER. - If you are requesting a range of indexed values from a table, or a - single indexed value that has multiple rows that match, - CLUSTER will help because once the index identifies the - table page for the first row that matches, all other rows - that match are probably already on the same table page, - and so you save disk accesses and speed up the query. - - - - CLUSTER can re-sort the table using either an index scan - on the specified index, or (if the index is a b-tree) a sequential - scan followed by sorting. It will attempt to choose the method that - will be faster, based on planner cost parameters and available statistical - information. - - While CLUSTER is running, the is temporarily changed to pg_catalog, pg_temp. - - When an index scan is used, a temporary copy of the table is created that - contains the table data in the index order. Temporary copies of each - index on the table are created as well. Therefore, you need free space on - disk at least equal to the sum of the table size and the index sizes. - - - - When a sequential scan and sort is used, a temporary sort file is - also created, so that the peak temporary space requirement is as much - as double the table size, plus the index sizes. This method is often - faster than the index scan method, but if the disk space requirement is - intolerable, you can disable this choice by temporarily setting to off. - - - - It is advisable to set to - a reasonably large value (but not more than the amount of RAM you can - dedicate to the CLUSTER operation) before clustering. - - - - Because the planner records statistics about the ordering of - tables, it is advisable to run ANALYZE - on the newly clustered table. - Otherwise, the planner might make poor choices of query plans. - - Because CLUSTER remembers which indexes are clustered, one can cluster the tables one wants clustered manually the first time, diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml new file mode 100644 index 00000000000..84f3c3e3f2b --- /dev/null +++ b/doc/src/sgml/ref/repack.sgml @@ -0,0 +1,254 @@ + + + + + REPACK + + + + REPACK + 7 + SQL - Language Statements + + + + REPACK + cluster a table according to an index + + + + +REPACK [ ( option [, ...] ) ] [ table_name [ USING INDEXindex_name ] ] + +where option can be one of: + + VERBOSE [ boolean ] + + + + + Description + + + REPACK reclaims storage occupied by dead + tuples. Unlike VACUUM, it does so by rewriting the + entire contents of the table specified + by table_name into a new disk + file with no extra space (except for the space guaranteed by + the fillfactor storage parameter), allowing unused space + to be returned to the operating system. + + + + Without + a table_name, REPACK + 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. + + + + If index_name is specified, + the table is clustered by this index. Please see the notes on clustering + below. + + + + 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. + + + + Notes on Clustering + + + When a table is clustered, it is physically reordered based on the index + information. Clustering is a one-time operation: when the table is + subsequently updated, the changes are not clustered. That is, no attempt + is made to store new or updated rows according to their index order. (If + one wishes, one can periodically recluster by issuing the command again. + Also, setting the table's fillfactor storage parameter + to less than 100% can aid in preserving cluster ordering during updates, + since updated rows are kept on the same page if enough space is available + there.) + + + + In cases where you are accessing single rows randomly within a table, the + actual order of the data in the table is unimportant. However, if you tend + to access some data more than others, and there is an index that groups + them together, you will benefit from using REPACK. If + you are requesting a range of indexed values from a table, or a single + indexed value that has multiple rows that match, + REPACK will help because once the index identifies the + table page for the first row that matches, all other rows that match are + probably already on the same table page, and so you save disk accesses and + speed up the query. + + + + REPACK can re-sort the table using either an index scan + on the specified index (if the index is a b-tree), or a sequential scan + followed by sorting. It will attempt to choose the method that will be + faster, based on planner cost parameters and available statistical + information. + + + + Because the planner records statistics about the ordering of tables, it is + advisable to + run ANALYZE on the + newly repacked table. Otherwise, the planner might make poor choices of + query plans. + + + + + Notes on Resources + + + When an index scan or a sequential scan without sort is used, a temporary + copy of the table is created that contains the table data in the index + order. Temporary copies of each index on the table are created as well. + Therefore, you need free space on disk at least equal to the sum of the + table size and the index sizes. + + + + When a sequential scan and sort is used, a temporary sort file is also + created, so that the peak temporary space requirement is as much as double + the table size, plus the index sizes. This method is often faster than + the index scan method, but if the disk space requirement is intolerable, + you can disable this choice by temporarily setting + to off. + + + + It is advisable to set to a + reasonably large value (but not more than the amount of RAM you can + dedicate to the REPACK operation) before repacking. + + + + + + + Parameters + + + + table_name + + + The name (possibly schema-qualified) of a table. + + + + + + index_name + + + The name of an index. + + + + + + VERBOSE + + + Prints a progress report as each table is clustered + at INFO level. + + + + + + boolean + + + Specifies whether the selected option should be turned on or off. + You can write TRUE, ON, or + 1 to enable the option, and FALSE, + OFF, or 0 to disable it. The + boolean value can also + be omitted, in which case TRUE is assumed. + + + + + + + + Notes + + + To repack a table, one must have the MAINTAIN privilege + on the table. + + + + While REPACK is running, the is temporarily changed to pg_catalog, + pg_temp. + + + + Each backend running REPACK will report its progress + in the pg_stat_progress_repack view. See + for details. + + + + Repacking a partitioned table repacks each of its partitions. If an index + is specified, each partition is clustered using the partition of that + index. REPACK on a partitioned table cannot be executed + inside a transaction block. + + + + + + Examples + + + Repack the table employees: + +REPACK employees; + + + + + + Cluster the table employees on the basis of its + index employees_ind: + +REPACK employees USING INDEX employees_ind; + + + + + Repack all tables in the database on which you have + the MAINTAIN privilege: + +REPACK; + + + + + Compatibility + + + There is no REPACK statement in the SQL standard. + + + + + diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml index bd5dcaf86a5..735a2a7703a 100644 --- a/doc/src/sgml/ref/vacuum.sgml +++ b/doc/src/sgml/ref/vacuum.sgml @@ -98,6 +98,14 @@ VACUUM [ ( option [, ...] ) ] [ FULL + + + + The FULL parameter is deprecated in favor of + . + + + Selects full vacuum, which can reclaim more space, but takes much longer and exclusively locks the table. diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..229912d35b7 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -195,6 +195,7 @@ &refreshMaterializedView; &reindex; &releaseSavepoint; + &repack; &reset; &revoke; &rollback; diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 24d3765aa20..18e349c3466 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -741,13 +741,13 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (OldIndex != NULL && !use_sort) { const int ci_index[] = { - PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_INDEX_RELID + PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_INDEX_RELID }; int64 ci_val[2]; /* Set phase and OIDOldIndex to columns */ - ci_val[0] = PROGRESS_CLUSTER_PHASE_INDEX_SCAN_HEAP; + ci_val[0] = PROGRESS_REPACK_PHASE_INDEX_SCAN_HEAP; ci_val[1] = RelationGetRelid(OldIndex); pgstat_progress_update_multi_param(2, ci_index, ci_val); @@ -759,15 +759,15 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, else { /* In scan-and-sort mode and also VACUUM FULL, set phase */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_SEQ_SCAN_HEAP); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP); tableScan = table_beginscan(OldHeap, SnapshotAny, 0, (ScanKey) NULL); heapScan = (HeapScanDesc) tableScan; indexScan = NULL; /* Set total heap blocks */ - pgstat_progress_update_param(PROGRESS_CLUSTER_TOTAL_HEAP_BLKS, + pgstat_progress_update_param(PROGRESS_REPACK_TOTAL_HEAP_BLKS, heapScan->rs_nblocks); } @@ -809,7 +809,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, * is manually updated to the correct value when the table * scan finishes. */ - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_BLKS_SCANNED, heapScan->rs_nblocks); break; } @@ -825,7 +825,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ if (prev_cblock != heapScan->rs_cblock) { - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_BLKS_SCANNED, (heapScan->rs_cblock + heapScan->rs_nblocks - heapScan->rs_startblock @@ -912,14 +912,14 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, * In scan-and-sort mode, report increase in number of tuples * scanned */ - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_SCANNED, *num_tuples); } else { const int ct_index[] = { - PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED, - PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN + PROGRESS_REPACK_HEAP_TUPLES_SCANNED, + PROGRESS_REPACK_HEAP_TUPLES_WRITTEN }; int64 ct_val[2]; @@ -952,14 +952,14 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, double n_tuples = 0; /* Report that we are now sorting tuples */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_SORT_TUPLES); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_SORT_TUPLES); tuplesort_performsort(tuplesort); /* Report that we are now writing new heap */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_WRITE_NEW_HEAP); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); for (;;) { @@ -977,7 +977,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, values, isnull, rwstate); /* Report n_tuples */ - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_WRITTEN, n_tuples); } diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 739a92bdcc1..466cf0fdef6 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -4079,7 +4079,7 @@ reindex_relation(const ReindexStmt *stmt, Oid relid, int flags, Assert(!ReindexIsProcessingIndex(indexOid)); /* Set index rebuild count */ - pgstat_progress_update_param(PROGRESS_CLUSTER_INDEX_REBUILD_COUNT, + pgstat_progress_update_param(PROGRESS_REPACK_INDEX_REBUILD_COUNT, i); i++; } diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 31d269b7ee0..5de46bcac52 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1262,6 +1262,33 @@ CREATE VIEW pg_stat_progress_cluster AS FROM pg_stat_get_progress_info('CLUSTER') AS S LEFT JOIN pg_database D ON S.datid = D.oid; +CREATE VIEW pg_stat_progress_repack AS + SELECT + S.pid AS pid, + S.datid AS datid, + D.datname AS datname, + S.relid AS relid, + CASE S.param1 WHEN 1 THEN 'REPACK' + END AS command, + CASE S.param2 WHEN 0 THEN 'initializing' + WHEN 1 THEN 'seq scanning heap' + 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' + 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 + FROM pg_stat_get_progress_info('REPACK') AS S + LEFT JOIN pg_database D ON S.datid = D.oid; + + CREATE VIEW pg_stat_progress_create_index AS SELECT S.pid AS pid, S.datid AS datid, D.datname AS datname, diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index 54a08e4102e..9ae3d87e412 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -46,6 +46,7 @@ #include "storage/lmgr.h" #include "storage/predicate.h" #include "utils/acl.h" +#include "utils/formatting.h" #include "utils/fmgroids.h" #include "utils/guc.h" #include "utils/inval.h" @@ -67,17 +68,33 @@ typedef struct Oid indexOid; } RelToCluster; - -static void cluster_multiple_rels(List *rtcs, ClusterParams *params); -static void rebuild_relation(Relation OldHeap, Relation index, bool verbose); +/* + * Map the value of ClusterCommand to string. + */ +#define CLUSTER_COMMAND_STR(cmd) ((cmd) == CLUSTER_COMMAND_CLUSTER ? \ + "cluster" : \ + ((cmd) == CLUSTER_COMMAND_REPACK ? \ + "repack" : "vacuum")) + +static void cluster_multiple_rels(List *rtcs, ClusterParams *params, + ClusterCommand cmd); +static void rebuild_relation(Relation OldHeap, Relation index, bool verbose, + ClusterCommand cmd); static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, - bool verbose, bool *pSwapToastByContent, + bool verbose, ClusterCommand cmd, + bool *pSwapToastByContent, TransactionId *pFreezeXid, MultiXactId *pCutoffMulti); static List *get_tables_to_cluster(MemoryContext cluster_context); +static List *get_tables_to_repack(MemoryContext repack_context); static List *get_tables_to_cluster_partitioned(MemoryContext cluster_context, - Oid indexOid); -static bool cluster_is_permitted_for_relation(Oid relid, Oid userid); - + Oid relid, bool rel_is_index, + ClusterCommand cmd); +static bool cluster_is_permitted_for_relation(Oid relid, Oid userid, + ClusterCommand cmd); +static Relation process_single_relation(RangeVar *relation, char *indexname, + ClusterCommand cmd, + ClusterParams *params, + Oid *indexOid_p); /*--------------------------------------------------------------------------- * This cluster code allows for clustering multiple tables at once. Because @@ -133,72 +150,11 @@ cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel) if (stmt->relation != NULL) { - /* This is the single-relation case. */ - Oid tableOid; - - /* - * Find, lock, and check permissions on the table. We obtain - * AccessExclusiveLock right away to avoid lock-upgrade hazard in the - * single-transaction case. - */ - tableOid = RangeVarGetRelidExtended(stmt->relation, - AccessExclusiveLock, - 0, - RangeVarCallbackMaintainsTable, - NULL); - rel = table_open(tableOid, NoLock); - - /* - * Reject clustering a remote temp table ... their local buffer - * manager is not going to cope. - */ - if (RELATION_IS_OTHER_TEMP(rel)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster temporary tables of other sessions"))); - - if (stmt->indexname == NULL) - { - ListCell *index; - - /* We need to find the index that has indisclustered set. */ - foreach(index, RelationGetIndexList(rel)) - { - indexOid = lfirst_oid(index); - if (get_index_isclustered(indexOid)) - break; - indexOid = InvalidOid; - } - - if (!OidIsValid(indexOid)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("there is no previously clustered index for table \"%s\"", - stmt->relation->relname))); - } - else - { - /* - * The index is expected to be in the same namespace as the - * relation. - */ - indexOid = get_relname_relid(stmt->indexname, - rel->rd_rel->relnamespace); - if (!OidIsValid(indexOid)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("index \"%s\" for table \"%s\" does not exist", - stmt->indexname, stmt->relation->relname))); - } - - /* For non-partitioned tables, do what we came here to do. */ - if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) - { - cluster_rel(rel, indexOid, ¶ms); - /* cluster_rel closes the relation, but keeps lock */ - + rel = process_single_relation(stmt->relation, stmt->indexname, + CLUSTER_COMMAND_CLUSTER, ¶ms, + &indexOid); + if (rel == NULL) return; - } } /* @@ -230,8 +186,11 @@ cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel) if (rel != NULL) { Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); - check_index_is_clusterable(rel, indexOid, AccessShareLock); - rtcs = get_tables_to_cluster_partitioned(cluster_context, indexOid); + check_index_is_clusterable(rel, indexOid, AccessShareLock, + CLUSTER_COMMAND_CLUSTER); + rtcs = get_tables_to_cluster_partitioned(cluster_context, indexOid, + true, + CLUSTER_COMMAND_CLUSTER); /* close relation, releasing lock on parent table */ table_close(rel, AccessExclusiveLock); @@ -243,7 +202,7 @@ cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel) } /* Do the job. */ - cluster_multiple_rels(rtcs, ¶ms); + cluster_multiple_rels(rtcs, ¶ms, CLUSTER_COMMAND_CLUSTER); /* Start a new transaction for the cleanup work. */ StartTransactionCommand(); @@ -260,7 +219,8 @@ cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel) * return. */ static void -cluster_multiple_rels(List *rtcs, ClusterParams *params) +cluster_multiple_rels(List *rtcs, ClusterParams *params, + ClusterCommand cmd) { ListCell *lc; @@ -283,7 +243,7 @@ cluster_multiple_rels(List *rtcs, ClusterParams *params) rel = table_open(rtc->tableOid, AccessExclusiveLock); /* Process this table */ - cluster_rel(rel, rtc->indexOid, params); + cluster_rel(rel, rtc->indexOid, params, cmd); /* cluster_rel closes the relation, but keeps lock */ PopActiveSnapshot(); @@ -306,9 +266,13 @@ cluster_multiple_rels(List *rtcs, ClusterParams *params) * If indexOid is InvalidOid, the table will be rewritten in physical order * instead of index order. This is the new implementation of VACUUM FULL, * and error messages should refer to the operation as VACUUM not CLUSTER. + * + * 'cmd' indicates which commands is being executed. REPACK should be the only + * caller of this function in the future. */ void -cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params) +cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, + ClusterCommand cmd) { Oid tableOid = RelationGetRelid(OldHeap); Oid save_userid; @@ -317,19 +281,33 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params) bool verbose = ((params->options & CLUOPT_VERBOSE) != 0); bool recheck = ((params->options & CLUOPT_RECHECK) != 0); Relation index; + const char *cmd_str = CLUSTER_COMMAND_STR(cmd); Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false)); /* Check for user-requested abort. */ CHECK_FOR_INTERRUPTS(); - pgstat_progress_start_command(PROGRESS_COMMAND_CLUSTER, tableOid); - if (OidIsValid(indexOid)) - pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND, + if (cmd == CLUSTER_COMMAND_REPACK) + pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid); + else + pgstat_progress_start_command(PROGRESS_COMMAND_CLUSTER, tableOid); + + if (cmd == CLUSTER_COMMAND_REPACK) + pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, + PROGRESS_REPACK_COMMAND_REPACK); + else if (OidIsValid(indexOid)) + { + Assert(cmd == CLUSTER_COMMAND_CLUSTER); + pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, PROGRESS_CLUSTER_COMMAND_CLUSTER); + } else - pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND, + { + Assert(cmd == CLUSTER_COMMAND_VACUUM); + pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, PROGRESS_CLUSTER_COMMAND_VACUUM_FULL); + } /* * Switch to the table owner's userid, so that any index functions are run @@ -353,7 +331,7 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params) if (recheck) { /* Check that the user still has privileges for the relation */ - if (!cluster_is_permitted_for_relation(tableOid, save_userid)) + if (!cluster_is_permitted_for_relation(tableOid, save_userid, cmd)) { relation_close(OldHeap, AccessExclusiveLock); goto out; @@ -403,39 +381,38 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params) * would work in most respects, but the index would only get marked as * indisclustered in the current database, leading to unexpected behavior * if CLUSTER were later invoked in another database. + * + * REPACK does not set indisclustered. XXX Not sure I understand the + * comment above: how can an attribute be set "only in the current + * database"? */ - if (OidIsValid(indexOid) && OldHeap->rd_rel->relisshared) + if (cmd == CLUSTER_COMMAND_CLUSTER && OldHeap->rd_rel->relisshared) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster a shared catalog"))); + errmsg("cannot %s a shared catalog", cmd_str))); /* * Don't process temp tables of other backends ... their local buffer * manager is not going to cope. */ if (RELATION_IS_OTHER_TEMP(OldHeap)) - { - if (OidIsValid(indexOid)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster temporary tables of other sessions"))); - else - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot vacuum temporary tables of other sessions"))); - } + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot %s temporary tables of other sessions", + cmd_str))); /* * Also check for active uses of the relation in the current transaction, * including open scans and pending AFTER trigger events. */ - CheckTableNotInUse(OldHeap, OidIsValid(indexOid) ? "CLUSTER" : "VACUUM"); + CheckTableNotInUse(OldHeap, asc_toupper(cmd_str, strlen(cmd_str))); /* Check heap and index are valid to cluster on */ if (OidIsValid(indexOid)) { /* verify the index is good and lock it */ - check_index_is_clusterable(OldHeap, indexOid, AccessExclusiveLock); + check_index_is_clusterable(OldHeap, indexOid, AccessExclusiveLock, + cmd); /* also open it */ index = index_open(indexOid, NoLock); } @@ -469,7 +446,7 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params) TransferPredicateLocksToHeapRelation(OldHeap); /* rebuild_relation does all the dirty work */ - rebuild_relation(OldHeap, index, verbose); + rebuild_relation(OldHeap, index, verbose, cmd); /* rebuild_relation closes OldHeap, and index if valid */ out: @@ -491,9 +468,11 @@ out: * protection here. */ void -check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode) +check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode, + ClusterCommand cmd) { Relation OldIndex; + const char *cmd_str = CLUSTER_COMMAND_STR(cmd); OldIndex = index_open(indexOid, lockmode); @@ -512,8 +491,8 @@ check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode) if (!OldIndex->rd_indam->amclusterable) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster on index \"%s\" because access method does not support clustering", - RelationGetRelationName(OldIndex)))); + errmsg("cannot %s on index \"%s\" because access method does not support clustering", + cmd_str, RelationGetRelationName(OldIndex)))); /* * Disallow clustering on incomplete indexes (those that might not index @@ -524,7 +503,8 @@ check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode) if (!heap_attisnull(OldIndex->rd_indextuple, Anum_pg_index_indpred, NULL)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster on partial index \"%s\"", + errmsg("cannot %s on partial index \"%s\"", + cmd_str, RelationGetRelationName(OldIndex)))); /* @@ -538,8 +518,8 @@ check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode) if (!OldIndex->rd_index->indisvalid) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster on invalid index \"%s\"", - RelationGetRelationName(OldIndex)))); + errmsg("cannot %s on invalid index \"%s\"", + cmd_str, RelationGetRelationName(OldIndex)))); /* Drop relcache refcnt on OldIndex, but keep lock */ index_close(OldIndex, NoLock); @@ -626,7 +606,8 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal) * On exit, they are closed, but locks on them are not released. */ static void -rebuild_relation(Relation OldHeap, Relation index, bool verbose) +rebuild_relation(Relation OldHeap, Relation index, bool verbose, + ClusterCommand cmd) { Oid tableOid = RelationGetRelid(OldHeap); Oid accessMethod = OldHeap->rd_rel->relam; @@ -664,7 +645,7 @@ 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, verbose, cmd, &swap_toast_by_content, &frozenXid, &cutoffMulti); @@ -829,8 +810,8 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod, */ static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verbose, - bool *pSwapToastByContent, TransactionId *pFreezeXid, - MultiXactId *pCutoffMulti) + ClusterCommand cmd, bool *pSwapToastByContent, + TransactionId *pFreezeXid, MultiXactId *pCutoffMulti) { Relation relRelation; HeapTuple reltup; @@ -845,6 +826,7 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb tups_recently_dead = 0; BlockNumber num_pages; int elevel = verbose ? INFO : DEBUG2; + const char *cmd_str = CLUSTER_COMMAND_STR(cmd); PGRUsage ru0; char *nspname; @@ -958,18 +940,21 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb /* Log what we're doing */ if (OldIndex != NULL && !use_sort) ereport(elevel, - (errmsg("clustering \"%s.%s\" using index scan on \"%s\"", + (errmsg("%sing \"%s.%s\" using index scan on \"%s\"", + cmd_str, nspname, RelationGetRelationName(OldHeap), RelationGetRelationName(OldIndex)))); else if (use_sort) ereport(elevel, - (errmsg("clustering \"%s.%s\" using sequential scan and sort", + (errmsg("%sing \"%s.%s\" using sequential scan and sort", + cmd_str, nspname, RelationGetRelationName(OldHeap)))); else ereport(elevel, - (errmsg("vacuuming \"%s.%s\"", + (errmsg("%sing \"%s.%s\"", + cmd_str, nspname, RelationGetRelationName(OldHeap)))); @@ -1458,8 +1443,8 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, int i; /* Report that we are now swapping relation files */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_SWAP_REL_FILES); /* Zero out possible results from swapped_relation_files */ memset(mapped_tables, 0, sizeof(mapped_tables)); @@ -1509,14 +1494,14 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT; /* Report that we are now reindexing relations */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_REBUILD_INDEX); + 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_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_FINAL_CLEANUP); /* * If the relation being rebuilt is pg_class, swap_relation_files() @@ -1666,7 +1651,8 @@ get_tables_to_cluster(MemoryContext cluster_context) index = (Form_pg_index) GETSTRUCT(indexTuple); - if (!cluster_is_permitted_for_relation(index->indrelid, GetUserId())) + if (!cluster_is_permitted_for_relation(index->indrelid, GetUserId(), + CLUSTER_COMMAND_CLUSTER)) continue; /* Use a permanent memory context for the result list */ @@ -1687,14 +1673,67 @@ get_tables_to_cluster(MemoryContext cluster_context) } /* - * Given an index on a partitioned table, return a list of RelToCluster for + * Like get_tables_to_cluster(), but do not care about indexes. + */ +static List * +get_tables_to_repack(MemoryContext repack_context) +{ + Relation relrelation; + TableScanDesc scan; + HeapTuple tuple; + MemoryContext old_context; + List *rtcs = NIL; + + /* + * Get all indexes that have indisclustered set and that the current user + * has the appropriate privileges for. + */ + relrelation = table_open(RelationRelationId, AccessShareLock); + scan = table_beginscan_catalog(relrelation, 0, NULL); + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + RelToCluster *rtc; + Form_pg_class relrelation = (Form_pg_class) GETSTRUCT(tuple); + Oid relid = relrelation->oid; + + /* Only interested in relations. */ + if (get_rel_relkind(relid) != RELKIND_RELATION) + continue; + + if (!cluster_is_permitted_for_relation(relid, GetUserId(), + CLUSTER_COMMAND_REPACK)) + continue; + + /* Use a permanent memory context for the result list */ + old_context = MemoryContextSwitchTo(repack_context); + + rtc = (RelToCluster *) palloc(sizeof(RelToCluster)); + rtc->tableOid = relid; + rtc->indexOid = InvalidOid; + rtcs = lappend(rtcs, rtc); + + MemoryContextSwitchTo(old_context); + } + table_endscan(scan); + + relation_close(relrelation, AccessShareLock); + + return rtcs; +} + +/* + * Given a partitioned table or its index, return a list of RelToCluster for * all the children leaves tables/indexes. * * Like expand_vacuum_rel, but here caller must hold AccessExclusiveLock * on the table containing the index. + * + * 'rel_is_index' tells whether 'relid' is that of an index (true) or of the + * owning relation. */ static List * -get_tables_to_cluster_partitioned(MemoryContext cluster_context, Oid indexOid) +get_tables_to_cluster_partitioned(MemoryContext cluster_context, Oid relid, + bool rel_is_index, ClusterCommand cmd) { List *inhoids; ListCell *lc; @@ -1702,17 +1741,33 @@ get_tables_to_cluster_partitioned(MemoryContext cluster_context, Oid indexOid) MemoryContext old_context; /* Do not lock the children until they're processed */ - inhoids = find_all_inheritors(indexOid, NoLock, NULL); + inhoids = find_all_inheritors(relid, NoLock, NULL); foreach(lc, inhoids) { - Oid indexrelid = lfirst_oid(lc); - Oid relid = IndexGetRelation(indexrelid, false); + Oid inhoid = lfirst_oid(lc); + Oid inhrelid, + inhindid; RelToCluster *rtc; - /* consider only leaf indexes */ - if (get_rel_relkind(indexrelid) != RELKIND_INDEX) - continue; + if (rel_is_index) + { + /* consider only leaf indexes */ + if (get_rel_relkind(inhoid) != RELKIND_INDEX) + continue; + + inhrelid = IndexGetRelation(inhoid, false); + inhindid = inhoid; + } + else + { + /* consider only leaf relations */ + if (get_rel_relkind(inhoid) != RELKIND_RELATION) + continue; + + inhrelid = inhoid; + inhindid = InvalidOid; + } /* * It's possible that the user does not have privileges to CLUSTER the @@ -1720,15 +1775,15 @@ get_tables_to_cluster_partitioned(MemoryContext cluster_context, Oid indexOid) * table. We skip any partitions which the user is not permitted to * CLUSTER. */ - if (!cluster_is_permitted_for_relation(relid, GetUserId())) + if (!cluster_is_permitted_for_relation(inhrelid, GetUserId(), cmd)) continue; /* Use a permanent memory context for the result list */ old_context = MemoryContextSwitchTo(cluster_context); rtc = (RelToCluster *) palloc(sizeof(RelToCluster)); - rtc->tableOid = relid; - rtc->indexOid = indexrelid; + rtc->tableOid = inhrelid; + rtc->indexOid = inhindid; rtcs = lappend(rtcs, rtc); MemoryContextSwitchTo(old_context); @@ -1742,13 +1797,192 @@ get_tables_to_cluster_partitioned(MemoryContext cluster_context, Oid indexOid) * function emits a WARNING. */ static bool -cluster_is_permitted_for_relation(Oid relid, Oid userid) +cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd) { if (pg_class_aclcheck(relid, userid, ACL_MAINTAIN) == ACLCHECK_OK) return true; ereport(WARNING, - (errmsg("permission denied to cluster \"%s\", skipping it", + (errmsg("permission denied to %s \"%s\", skipping it", + CLUSTER_COMMAND_STR(cmd), get_rel_name(relid)))); return false; } + +/* + * REPACK is intended to be a replacement of both CLUSTER and VACUUM FULL. + */ +void +repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) +{ + ListCell *lc; + ClusterParams params = {0}; + bool verbose = false; + Relation rel = NULL; + Oid indexOid = InvalidOid; + MemoryContext repack_context; + List *rtcs; + + /* Parse option list */ + foreach(lc, stmt->params) + { + DefElem *opt = (DefElem *) lfirst(lc); + + if (strcmp(opt->defname, "verbose") == 0) + verbose = defGetBoolean(opt); + else + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unrecognized REPACK option \"%s\"", + opt->defname), + parser_errposition(pstate, opt->location))); + } + + params.options = (verbose ? CLUOPT_VERBOSE : 0); + + if (stmt->relation != NULL) + { + rel = process_single_relation(stmt->relation, stmt->indexname, + CLUSTER_COMMAND_REPACK, ¶ms, + &indexOid); + if (rel == NULL) + return; + } + + /* + * 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. + */ + PreventInTransactionBlock(isTopLevel, "REPACK"); + + /* Also, we need a memory context to hold our list of relations */ + repack_context = AllocSetContextCreate(PortalContext, + "Repack", + ALLOCSET_DEFAULT_SIZES); + + params.options |= CLUOPT_RECHECK; + if (rel != NULL) + { + Oid relid; + bool rel_is_index; + + Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + + if (OidIsValid(indexOid)) + { + relid = indexOid; + rel_is_index = true; + } + else + { + relid = RelationGetRelid(rel); + rel_is_index = false; + } + rtcs = get_tables_to_cluster_partitioned(repack_context, relid, + rel_is_index, + CLUSTER_COMMAND_REPACK); + + /* close relation, releasing lock on parent table */ + table_close(rel, AccessExclusiveLock); + } + else + rtcs = get_tables_to_repack(repack_context); + + /* Do the job. */ + cluster_multiple_rels(rtcs, ¶ms, CLUSTER_COMMAND_REPACK); + + /* Start a new transaction for the cleanup work. */ + StartTransactionCommand(); + + /* Clean up working storage */ + MemoryContextDelete(repack_context); + +} + +/* + * REPACK a single relation. + * + * Return NULL if done, relation reference if the caller needs to process it + * (because the relation is partitioned). + */ +static Relation +process_single_relation(RangeVar *relation, char *indexname, + ClusterCommand cmd, ClusterParams *params, + Oid *indexOid_p) +{ + Relation rel; + Oid indexOid = InvalidOid; + + /* This is the single-relation case. */ + Oid tableOid; + + /* + * Find, lock, and check permissions on the table. We obtain + * AccessExclusiveLock right away to avoid lock-upgrade hazard in the + * single-transaction case. + */ + tableOid = RangeVarGetRelidExtended(relation, + AccessExclusiveLock, + 0, + RangeVarCallbackMaintainsTable, + NULL); + rel = table_open(tableOid, NoLock); + + /* + * Reject clustering a remote temp table ... their local buffer manager is + * not going to cope. + */ + if (RELATION_IS_OTHER_TEMP(rel)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot %s temporary tables of other sessions", + CLUSTER_COMMAND_STR(cmd)))); + + if (indexname == NULL && cmd == CLUSTER_COMMAND_CLUSTER) + { + ListCell *index; + + /* We need to find the index that has indisclustered set. */ + foreach(index, RelationGetIndexList(rel)) + { + indexOid = lfirst_oid(index); + if (get_index_isclustered(indexOid)) + break; + indexOid = InvalidOid; + } + + if (!OidIsValid(indexOid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("there is no previously clustered index for table \"%s\"", + relation->relname))); + } + else if (indexname != NULL) + { + /* + * The index is expected to be in the same namespace as the relation. + */ + indexOid = get_relname_relid(indexname, + rel->rd_rel->relnamespace); + if (!OidIsValid(indexOid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("index \"%s\" for table \"%s\" does not exist", + indexname, relation->relname))); + } + + *indexOid_p = indexOid; + + /* For non-partitioned tables, do what we came here to do. */ + if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) + { + cluster_rel(rel, indexOid, params, cmd); + /* cluster_rel closes the relation, but keeps lock */ + + return NULL; + } + + return rel; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 10624353b0a..b7a74f25785 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -15844,7 +15844,8 @@ ATExecClusterOn(Relation rel, const char *indexName, LOCKMODE lockmode) indexName, RelationGetRelationName(rel)))); /* Check index is valid to cluster on */ - check_index_is_clusterable(rel, indexOid, lockmode); + check_index_is_clusterable(rel, indexOid, lockmode, + CLUSTER_COMMAND_CLUSTER); /* And do the work */ mark_index_clustered(rel, indexOid, false); diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index db5da3ce826..a4ad23448f8 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -2263,7 +2263,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, cluster_params.options |= CLUOPT_VERBOSE; /* VACUUM FULL is now a variant of CLUSTER; see cluster.c */ - cluster_rel(rel, InvalidOid, &cluster_params); + cluster_rel(rel, InvalidOid, &cluster_params, + CLUSTER_COMMAND_VACUUM); /* cluster_rel closes the relation, but keeps lock */ rel = NULL; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0fc502a3a40..9c79265a438 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -298,7 +298,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt - RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt + RemoveFuncStmt RemoveOperStmt RenameStmt RepackStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt SecLabelStmt SelectStmt TransactionStmt TransactionStmtLegacy TruncateStmt UnlistenStmt UpdateStmt VacuumStmt @@ -381,7 +381,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type copy_file_name access_method_clause attr_name table_access_method_clause name cursor_name file_name - cluster_index_specification + cluster_index_specification repack_index_specification %type func_name handler_name qual_Op qual_all_Op subquery_Op opt_inline_handler opt_validator validator_clause @@ -764,7 +764,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); QUOTE QUOTES RANGE READ REAL REASSIGN RECURSIVE REF_P REFERENCES REFERENCING - REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA + REFRESH REINDEX RELATIVE_P RELEASE RENAME REPACK REPEATABLE REPLACE REPLICA RESET RESTART RESTRICT RETURN RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP ROUTINE ROUTINES ROW ROWS RULE @@ -1100,6 +1100,7 @@ stmt: | RemoveFuncStmt | RemoveOperStmt | RenameStmt + | RepackStmt | RevokeStmt | RevokeRoleStmt | RuleStmt @@ -11887,6 +11888,60 @@ cluster_index_specification: | /*EMPTY*/ { $$ = NULL; } ; +/***************************************************************************** + * + * QUERY: + * REPACK [ (options) ] [ [ USING INDEX ] ] + * + *****************************************************************************/ + +RepackStmt: + REPACK qualified_name repack_index_specification + { + RepackStmt *n = makeNode(RepackStmt); + + n->relation = $2; + n->indexname = $3; + n->params = NIL; + $$ = (Node *) n; + } + + | REPACK '(' utility_option_list ')' qualified_name repack_index_specification + { + RepackStmt *n = makeNode(RepackStmt); + + n->relation = $5; + n->indexname = $6; + n->params = $3; + $$ = (Node *) n; + } + + | REPACK + { + RepackStmt *n = makeNode(RepackStmt); + + n->relation = NULL; + n->indexname = NULL; + n->params = NIL; + $$ = (Node *) n; + } + + | REPACK '(' utility_option_list ')' + { + RepackStmt *n = makeNode(RepackStmt); + + n->relation = NULL; + n->indexname = NULL; + n->params = $3; + $$ = (Node *) n; + } + ; + +repack_index_specification: + USING INDEX name { $$ = $3; } + | /*EMPTY*/ { $$ = NULL; } + ; + /***************************************************************************** * @@ -17927,6 +17982,7 @@ unreserved_keyword: | RELATIVE_P | RELEASE | RENAME + | REPACK | REPEATABLE | REPLACE | REPLICA @@ -18558,6 +18614,7 @@ bare_label_keyword: | RELATIVE_P | RELEASE | RENAME + | REPACK | REPEATABLE | REPLACE | REPLICA diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 25fe3d58016..bf3ba3c2ae7 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -280,6 +280,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_ClusterStmt: case T_ReindexStmt: case T_VacuumStmt: + case T_RepackStmt: { /* * These commands write WAL, so they're not strictly @@ -862,6 +863,10 @@ standard_ProcessUtility(PlannedStmt *pstmt, ExecVacuum(pstate, (VacuumStmt *) parsetree, isTopLevel); break; + case T_RepackStmt: + repack(pstate, (RepackStmt *) parsetree, isTopLevel); + break; + case T_ExplainStmt: ExplainQuery(pstate, (ExplainStmt *) parsetree, params, dest); break; @@ -2869,6 +2874,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_ANALYZE; break; + case T_RepackStmt: + tag = CMDTAG_REPACK; + break; + case T_ExplainStmt: tag = CMDTAG_EXPLAIN; break; diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 97af7c6554f..ddec4914ea5 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -268,6 +268,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS) cmdtype = PROGRESS_COMMAND_ANALYZE; else if (pg_strcasecmp(cmd, "CLUSTER") == 0) cmdtype = PROGRESS_COMMAND_CLUSTER; + else if (pg_strcasecmp(cmd, "REPACK") == 0) + cmdtype = PROGRESS_COMMAND_REPACK; else if (pg_strcasecmp(cmd, "CREATE INDEX") == 0) cmdtype = PROGRESS_COMMAND_CREATE_INDEX; else if (pg_strcasecmp(cmd, "BASEBACKUP") == 0) diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 98951aef82c..31271786f21 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1223,7 +1223,7 @@ static const char *const sql_commands[] = { "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", - "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", + "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "REPACK", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", "TABLE", "TRUNCATE", "UNLISTEN", "UPDATE", "VACUUM", "VALUES", "WITH", @@ -4913,6 +4913,35 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); } +/* REPACK */ + else if (Matches("REPACK")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables); + else if (Matches("REPACK", "(*)")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables); + /* If we have REPACK , then add "USING INDEX" */ + else if (Matches("REPACK", MatchAnyExcept("("))) + COMPLETE_WITH("USING INDEX"); + /* If we have REPACK (*) , then add "USING INDEX" */ + else if (Matches("REPACK", "(*)", MatchAny)) + COMPLETE_WITH("USING INDEX"); + /* If we have REPACK USING, then add the index as well */ + else if (Matches("REPACK", MatchAny, "USING", "INDEX")) + { + set_completion_reference(prev3_wd); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table); + } + else if (HeadMatches("REPACK", "(*") && + !HeadMatches("REPACK", "(*)")) + { + /* + * This fires if we're in an unfinished parenthesized option list. + * get_previous_words treats a completed parenthesized option list as + * one word, so the above test is correct. + */ + if (ends_with(prev_wd, '(') || ends_with(prev_wd, ',')) + COMPLETE_WITH("VERBOSE"); + } + /* SECURITY LABEL */ else if (Matches("SECURITY")) COMPLETE_WITH("LABEL"); diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h index 60088a64cbb..c2976905e4d 100644 --- a/src/include/commands/cluster.h +++ b/src/include/commands/cluster.h @@ -31,10 +31,27 @@ typedef struct ClusterParams bits32 options; /* bitmask of CLUOPT_* */ } ClusterParams; +/* + * cluster.c currently implements three nearly identical commands: CLUSTER, + * VACUUM FULL and REPACK. Where needed, use this enumeration to distinguish + * which of these commands is being executed. + * + * Remove this stuff when removing the (now deprecated) CLUSTER and VACUUM + * FULL commands. + */ +typedef enum ClusterCommand +{ + CLUSTER_COMMAND_CLUSTER, + CLUSTER_COMMAND_REPACK, + CLUSTER_COMMAND_VACUUM +} ClusterCommand; + extern void cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel); -extern void cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params); +extern void cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, + ClusterCommand cmd); extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid, - LOCKMODE lockmode); + LOCKMODE lockmode, + ClusterCommand cmd); extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal); extern Oid make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod, @@ -48,4 +65,5 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, MultiXactId cutoffMulti, char newrelpersistence); +extern void repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel); #endif /* CLUSTER_H */ diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h index 7c736e7b03b..7644267e14f 100644 --- a/src/include/commands/progress.h +++ b/src/include/commands/progress.h @@ -56,24 +56,48 @@ #define PROGRESS_ANALYZE_PHASE_COMPUTE_EXT_STATS 4 #define PROGRESS_ANALYZE_PHASE_FINALIZE_ANALYZE 5 -/* Progress parameters for cluster */ -#define PROGRESS_CLUSTER_COMMAND 0 -#define PROGRESS_CLUSTER_PHASE 1 -#define PROGRESS_CLUSTER_INDEX_RELID 2 -#define PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED 3 -#define PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN 4 -#define PROGRESS_CLUSTER_TOTAL_HEAP_BLKS 5 -#define PROGRESS_CLUSTER_HEAP_BLKS_SCANNED 6 -#define PROGRESS_CLUSTER_INDEX_REBUILD_COUNT 7 - -/* Phases of cluster (as advertised via PROGRESS_CLUSTER_PHASE) */ -#define PROGRESS_CLUSTER_PHASE_SEQ_SCAN_HEAP 1 -#define PROGRESS_CLUSTER_PHASE_INDEX_SCAN_HEAP 2 -#define PROGRESS_CLUSTER_PHASE_SORT_TUPLES 3 -#define PROGRESS_CLUSTER_PHASE_WRITE_NEW_HEAP 4 -#define PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES 5 -#define PROGRESS_CLUSTER_PHASE_REBUILD_INDEX 6 -#define PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP 7 +/* + * Progress parameters for REPACK. + * + * Note: Since REPACK shares some code with CLUSTER, (some of) these values + * are also used by CLUSTER. (CLUSTER is now deprecated, so it makes no sense + * to introduce separate set of constants.) + */ +#define PROGRESS_REPACK_COMMAND 0 +#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 + +/* + * Phases of repack (as advertised via PROGRESS_REPACK_PHASE). + * + * Note: Since REPACK shares some code with CLUSTER, (some of) these values + * are also used by CLUSTER. (CLUSTER is now deprecated, so it makes no sense + * to introduce separate set of constants.) + */ +#define PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP 1 +#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 + +/* Commands of PROGRESS_REPACK */ +#define PROGRESS_REPACK_COMMAND_REPACK 1 + +/* + * Progress parameters for cluster. + * + * Although we need to report REPACK and CLUSTER in separate views, the + * parameters and phases of CLUSTER are a subset of those of REPACK. Therefore + * we just use the appropriate values defined for REPACK above instead of + * defining a separate set of constants here. + */ /* Commands of PROGRESS_CLUSTER */ #define PROGRESS_CLUSTER_COMMAND_CLUSTER 1 diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index df331b1c0d9..4ef76c852f5 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3921,6 +3921,19 @@ typedef struct ClusterStmt List *params; /* list of DefElem nodes */ } ClusterStmt; +/* ---------------------- + * Repack Statement + * ---------------------- + */ +typedef struct RepackStmt +{ + NodeTag type; + RangeVar *relation; /* relation being repacked */ + char *indexname; /* order tuples by this index */ + List *params; /* list of DefElem nodes */ +} RepackStmt; + + /* ---------------------- * Vacuum and Analyze Statements * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 40cf090ce61..0932d6fce5b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -373,6 +373,7 @@ PG_KEYWORD("reindex", REINDEX, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("relative", RELATIVE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("release", RELEASE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("rename", RENAME, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("repack", REPACK, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("repeatable", REPEATABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("replace", REPLACE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("replica", REPLICA, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..cceb312f2b3 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -196,6 +196,7 @@ PG_CMDTAG(CMDTAG_REASSIGN_OWNED, "REASSIGN OWNED", false, false, false) PG_CMDTAG(CMDTAG_REFRESH_MATERIALIZED_VIEW, "REFRESH MATERIALIZED VIEW", true, false, false) PG_CMDTAG(CMDTAG_REINDEX, "REINDEX", true, false, false) PG_CMDTAG(CMDTAG_RELEASE, "RELEASE", false, false, false) +PG_CMDTAG(CMDTAG_REPACK, "REPACK", false, false, false) PG_CMDTAG(CMDTAG_RESET, "RESET", false, false, false) PG_CMDTAG(CMDTAG_REVOKE, "REVOKE", true, false, false) PG_CMDTAG(CMDTAG_REVOKE_ROLE, "REVOKE ROLE", false, false, false) diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h index dda813ab407..da3d14bb97b 100644 --- a/src/include/utils/backend_progress.h +++ b/src/include/utils/backend_progress.h @@ -25,6 +25,7 @@ typedef enum ProgressCommandType PROGRESS_COMMAND_VACUUM, PROGRESS_COMMAND_ANALYZE, PROGRESS_COMMAND_CLUSTER, + PROGRESS_COMMAND_REPACK, PROGRESS_COMMAND_CREATE_INDEX, PROGRESS_COMMAND_BASEBACKUP, PROGRESS_COMMAND_COPY, diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expected/cluster.out index 4d40a6809ab..ed7df29b8e5 100644 --- a/src/test/regress/expected/cluster.out +++ b/src/test/regress/expected/cluster.out @@ -254,6 +254,120 @@ ORDER BY 1; clstr_tst_pkey (3 rows) +-- REPACK handles individual tables identically to CLUSTER, but it's worth +-- checking if it handles table hierarchies identically as well. +REPACK clstr_tst USING INDEX clstr_tst_c; +-- Verify that inheritance link still works +INSERT INTO clstr_tst_inh VALUES (0, 100, 'in child table 2'); +SELECT a,b,c,substring(d for 30), length(d) from clstr_tst; + a | b | c | substring | length +----+-----+------------------+--------------------------------+-------- + 10 | 14 | catorce | | + 18 | 5 | cinco | | + 9 | 4 | cuatro | | + 26 | 19 | diecinueve | | + 12 | 18 | dieciocho | | + 30 | 16 | dieciseis | | + 24 | 17 | diecisiete | | + 2 | 10 | diez | | + 23 | 12 | doce | | + 11 | 2 | dos | | + 25 | 9 | nueve | | + 31 | 8 | ocho | | + 1 | 11 | once | | + 28 | 15 | quince | | + 32 | 6 | seis | xyzzyxyzzyxyzzyxyzzyxyzzyxyzzy | 500000 + 29 | 7 | siete | | + 15 | 13 | trece | | + 22 | 30 | treinta | | + 17 | 32 | treinta y dos | | + 3 | 31 | treinta y uno | | + 5 | 3 | tres | | + 20 | 1 | uno | | + 6 | 20 | veinte | | + 14 | 25 | veinticinco | | + 21 | 24 | veinticuatro | | + 4 | 22 | veintidos | | + 19 | 29 | veintinueve | | + 16 | 28 | veintiocho | | + 27 | 26 | veintiseis | | + 13 | 27 | veintisiete | | + 7 | 23 | veintitres | | + 8 | 21 | veintiuno | | + 0 | 100 | in child table | | + 0 | 100 | in child table 2 | | +(34 rows) + +-- Verify that foreign key link still works +INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail'); +ERROR: insert or update on table "clstr_tst" violates foreign key constraint "clstr_tst_con" +DETAIL: Key (b)=(1111) is not present in table "clstr_tst_s". +SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass +ORDER BY 1; + conname +---------------------- + clstr_tst_a_not_null + clstr_tst_con + clstr_tst_pkey +(3 rows) + +-- Yet another code path: REPACK w/o index. +REPACK clstr_tst USING INDEX clstr_tst_c; +-- Verify that inheritance link still works +INSERT INTO clstr_tst_inh VALUES (0, 100, 'in child table 3'); +SELECT a,b,c,substring(d for 30), length(d) from clstr_tst; + a | b | c | substring | length +----+-----+------------------+--------------------------------+-------- + 10 | 14 | catorce | | + 18 | 5 | cinco | | + 9 | 4 | cuatro | | + 26 | 19 | diecinueve | | + 12 | 18 | dieciocho | | + 30 | 16 | dieciseis | | + 24 | 17 | diecisiete | | + 2 | 10 | diez | | + 23 | 12 | doce | | + 11 | 2 | dos | | + 25 | 9 | nueve | | + 31 | 8 | ocho | | + 1 | 11 | once | | + 28 | 15 | quince | | + 32 | 6 | seis | xyzzyxyzzyxyzzyxyzzyxyzzyxyzzy | 500000 + 29 | 7 | siete | | + 15 | 13 | trece | | + 22 | 30 | treinta | | + 17 | 32 | treinta y dos | | + 3 | 31 | treinta y uno | | + 5 | 3 | tres | | + 20 | 1 | uno | | + 6 | 20 | veinte | | + 14 | 25 | veinticinco | | + 21 | 24 | veinticuatro | | + 4 | 22 | veintidos | | + 19 | 29 | veintinueve | | + 16 | 28 | veintiocho | | + 27 | 26 | veintiseis | | + 13 | 27 | veintisiete | | + 7 | 23 | veintitres | | + 8 | 21 | veintiuno | | + 0 | 100 | in child table | | + 0 | 100 | in child table 2 | | + 0 | 100 | in child table 3 | | +(35 rows) + +-- Verify that foreign key link still works +INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail'); +ERROR: insert or update on table "clstr_tst" violates foreign key constraint "clstr_tst_con" +DETAIL: Key (b)=(1111) is not present in table "clstr_tst_s". +SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass +ORDER BY 1; + conname +---------------------- + clstr_tst_a_not_null + clstr_tst_con + clstr_tst_pkey +(3 rows) + SELECT relname, relkind, EXISTS(SELECT 1 FROM pg_class WHERE oid = c.reltoastrelid) AS hastoast FROM pg_class c WHERE relname LIKE 'clstr_tst%' ORDER BY relname; @@ -381,6 +495,35 @@ SELECT * FROM clstr_1; 2 (2 rows) +-- REPACK w/o argument performs no ordering, so we can only check which tables +-- have the relfilenode changed. +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_old AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); +SET SESSION AUTHORIZATION regress_clstr_user; +SET client_min_messages = ERROR; -- order of "skipping" warnings may vary +REPACK; +RESET client_min_messages; +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_new AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); +-- Do the actual comparison. Unlike CLUSTER, clstr_3 should have been +-- processed because there is nothing like clustering index here. +SELECT o.relname FROM relnodes_old o +JOIN relnodes_new n ON o.relname = n.relname +WHERE o.relfilenode <> n.relfilenode +ORDER BY o.relname; + relname +--------- + clstr_1 + clstr_3 +(2 rows) + +SET SESSION AUTHORIZATION regress_clstr_user; -- Test MVCC-safety of cluster. There isn't much we can do to verify the -- results with a single backend... CREATE TABLE clustertest (key int PRIMARY KEY); @@ -495,6 +638,43 @@ ALTER TABLE clstrpart SET WITHOUT CLUSTER; ERROR: cannot mark index clustered in partitioned table ALTER TABLE clstrpart CLUSTER ON clstrpart_idx; ERROR: cannot mark index clustered in partitioned table +-- Check that REPACK sets new relfilenodes: it should process exactly the same +-- tables as CLUSTER did. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +REPACK clstrpart USING INDEX clstrpart_idx; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname) ORDER BY relname COLLATE "C"; + relname | level | relkind | ?column? +-------------+-------+---------+---------- + clstrpart | 0 | p | t + clstrpart1 | 1 | p | t + clstrpart11 | 2 | r | f + clstrpart12 | 2 | p | t + clstrpart2 | 1 | r | f + clstrpart3 | 1 | p | t + clstrpart33 | 2 | r | f +(7 rows) + +-- And finally the same for REPACK w/o index. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +REPACK clstrpart; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname) ORDER BY relname COLLATE "C"; + relname | level | relkind | ?column? +-------------+-------+---------+---------- + clstrpart | 0 | p | t + clstrpart1 | 1 | p | t + clstrpart11 | 2 | r | f + clstrpart12 | 2 | p | t + clstrpart2 | 1 | r | f + clstrpart3 | 1 | p | t + clstrpart33 | 2 | r | f +(7 rows) + DROP TABLE clstrpart; -- Ownership of partitions is checked CREATE TABLE ptnowner(i int unique) PARTITION BY LIST (i); diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 47478969135..84ca2dc3778 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -2041,6 +2041,33 @@ pg_stat_progress_create_index| SELECT s.pid, s.param15 AS partitions_done FROM (pg_stat_get_progress_info('CREATE INDEX'::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_repack| SELECT s.pid, + s.datid, + d.datname, + s.relid, + CASE s.param1 + WHEN 1 THEN 'REPACK'::text + ELSE NULL::text + END AS command, + CASE s.param2 + WHEN 0 THEN 'initializing'::text + WHEN 1 THEN 'seq scanning heap'::text + 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 + 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 + 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, s.datid, d.datname, diff --git a/src/test/regress/sql/cluster.sql b/src/test/regress/sql/cluster.sql index b7115f86104..e348e26fbfa 100644 --- a/src/test/regress/sql/cluster.sql +++ b/src/test/regress/sql/cluster.sql @@ -76,6 +76,33 @@ INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail'); SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass ORDER BY 1; +-- REPACK handles individual tables identically to CLUSTER, but it's worth +-- checking if it handles table hierarchies identically as well. +REPACK clstr_tst USING INDEX clstr_tst_c; + +-- Verify that inheritance link still works +INSERT INTO clstr_tst_inh VALUES (0, 100, 'in child table 2'); +SELECT a,b,c,substring(d for 30), length(d) from clstr_tst; + +-- Verify that foreign key link still works +INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail'); + +SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass +ORDER BY 1; + +-- Yet another code path: REPACK w/o index. +REPACK clstr_tst USING INDEX clstr_tst_c; + +-- Verify that inheritance link still works +INSERT INTO clstr_tst_inh VALUES (0, 100, 'in child table 3'); +SELECT a,b,c,substring(d for 30), length(d) from clstr_tst; + +-- Verify that foreign key link still works +INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail'); + +SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass +ORDER BY 1; + SELECT relname, relkind, EXISTS(SELECT 1 FROM pg_class WHERE oid = c.reltoastrelid) AS hastoast @@ -159,6 +186,34 @@ INSERT INTO clstr_1 VALUES (1); CLUSTER clstr_1; SELECT * FROM clstr_1; +-- REPACK w/o argument performs no ordering, so we can only check which tables +-- have the relfilenode changed. +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_old AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); + +SET SESSION AUTHORIZATION regress_clstr_user; +SET client_min_messages = ERROR; -- order of "skipping" warnings may vary +REPACK; +RESET client_min_messages; + +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_new AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); + +-- Do the actual comparison. Unlike CLUSTER, clstr_3 should have been +-- processed because there is nothing like clustering index here. +SELECT o.relname FROM relnodes_old o +JOIN relnodes_new n ON o.relname = n.relname +WHERE o.relfilenode <> n.relfilenode +ORDER BY o.relname; + +SET SESSION AUTHORIZATION regress_clstr_user; + -- Test MVCC-safety of cluster. There isn't much we can do to verify the -- results with a single backend... @@ -229,6 +284,24 @@ SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM o CLUSTER clstrpart; ALTER TABLE clstrpart SET WITHOUT CLUSTER; ALTER TABLE clstrpart CLUSTER ON clstrpart_idx; + +-- Check that REPACK sets new relfilenodes: it should process exactly the same +-- tables as CLUSTER did. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +REPACK clstrpart USING INDEX clstrpart_idx; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname) ORDER BY relname COLLATE "C"; + +-- And finally the same for REPACK w/o index. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +REPACK clstrpart; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname) ORDER BY relname COLLATE "C"; + DROP TABLE clstrpart; -- Ownership of partitions is checked diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index b66cecd8799..c7ea8fb93ca 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -416,6 +416,7 @@ ClientCertName ClientConnectionInfo ClientData ClientSocket +ClusterCommand ClonePtrType ClosePortalStmt ClosePtrType @@ -2506,6 +2507,7 @@ ReorderBufferTupleCidKey ReorderBufferUpdateProgressTxnCB ReorderTuple RepOriginId +RepackStmt ReparameterizeForeignPathByChild_function ReplaceVarsFromTargetList_context ReplaceVarsNoMatchOption -- 2.43.5 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v12-0002-Move-conversion-of-a-historic-to-MVCC-snapshot-to-a-.patch From 6afa32d3ea9d92d4eccd7a9befb2d4991418e649 Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Tue, 1 Apr 2025 13:48:57 +0200 Subject: [PATCH 2/9] Move conversion of a "historic" to MVCC snapshot to a separate function. The conversion is now handled by SnapBuildMVCCFromHistoric(). REPACK CONCURRENTLY will also need it. --- src/backend/replication/logical/snapbuild.c | 51 +++++++++++++++++---- src/backend/utils/time/snapmgr.c | 3 +- src/include/replication/snapbuild.h | 1 + src/include/utils/snapmgr.h | 1 + 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c index b64e53de017..e5d2a583ce6 100644 --- a/src/backend/replication/logical/snapbuild.c +++ b/src/backend/replication/logical/snapbuild.c @@ -440,10 +440,7 @@ Snapshot SnapBuildInitialSnapshot(SnapBuild *builder) { Snapshot snap; - TransactionId xid; TransactionId safeXid; - TransactionId *newxip; - int newxcnt = 0; Assert(XactIsoLevel == XACT_REPEATABLE_READ); Assert(builder->building_full_snapshot); @@ -485,6 +482,31 @@ SnapBuildInitialSnapshot(SnapBuild *builder) MyProc->xmin = snap->xmin; + /* Convert the historic snapshot to MVCC snapshot. */ + return SnapBuildMVCCFromHistoric(snap, true); +} + +/* + * Turn a historic MVCC snapshot into an ordinary MVCC snapshot. + * + * Unlike a regular (non-historic) MVCC snapshot, the xip array of this + * snapshot contains not only running main transactions, but also their + * subtransactions. This difference does has no impact on XidInMVCCSnapshot(). + * + * Pass true for 'in_place' if you don't care about modifying the source + * snapshot. If you need a new instance, and one that was allocated as a + * single chunk of memory, pass false. + */ +Snapshot +SnapBuildMVCCFromHistoric(Snapshot snapshot, bool in_place) +{ + TransactionId xid; + TransactionId *oldxip = snapshot->xip; + uint32 oldxcnt = snapshot->xcnt; + TransactionId *newxip; + int newxcnt = 0; + Snapshot result; + /* allocate in transaction context */ newxip = (TransactionId *) palloc(sizeof(TransactionId) * GetMaxSnapshotXidCount()); @@ -495,7 +517,7 @@ SnapBuildInitialSnapshot(SnapBuild *builder) * classical snapshot by marking all non-committed transactions as * in-progress. This can be expensive. */ - for (xid = snap->xmin; NormalTransactionIdPrecedes(xid, snap->xmax);) + for (xid = snapshot->xmin; NormalTransactionIdPrecedes(xid, snapshot->xmax);) { void *test; @@ -503,7 +525,7 @@ SnapBuildInitialSnapshot(SnapBuild *builder) * Check whether transaction committed using the decoding snapshot * meaning of ->xip. */ - test = bsearch(&xid, snap->xip, snap->xcnt, + test = bsearch(&xid, snapshot->xip, snapshot->xcnt, sizeof(TransactionId), xidComparator); if (test == NULL) @@ -520,11 +542,22 @@ SnapBuildInitialSnapshot(SnapBuild *builder) } /* adjust remaining snapshot fields as needed */ - snap->snapshot_type = SNAPSHOT_MVCC; - snap->xcnt = newxcnt; - snap->xip = newxip; + snapshot->xcnt = newxcnt; + snapshot->xip = newxip; + + if (in_place) + result = snapshot; + else + { + result = CopySnapshot(snapshot); + + /* Restore the original values so the source is intact. */ + snapshot->xip = oldxip; + snapshot->xcnt = oldxcnt; + } + result->snapshot_type = SNAPSHOT_MVCC; - return snap; + return result; } /* diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c index ea35f30f494..70a6b8902d1 100644 --- a/src/backend/utils/time/snapmgr.c +++ b/src/backend/utils/time/snapmgr.c @@ -212,7 +212,6 @@ typedef struct ExportedSnapshot static List *exportedSnapshots = NIL; /* Prototypes for local functions */ -static Snapshot CopySnapshot(Snapshot snapshot); static void UnregisterSnapshotNoOwner(Snapshot snapshot); static void FreeSnapshot(Snapshot snapshot); static void SnapshotResetXmin(void); @@ -591,7 +590,7 @@ SetTransactionSnapshot(Snapshot sourcesnap, VirtualTransactionId *sourcevxid, * The copy is palloc'd in TopTransactionContext and has initial refcounts set * to 0. The returned snapshot has the copied flag set. */ -static Snapshot +Snapshot CopySnapshot(Snapshot snapshot) { Snapshot newsnap; diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h index 44031dcf6e3..6d4d2d1814c 100644 --- a/src/include/replication/snapbuild.h +++ b/src/include/replication/snapbuild.h @@ -73,6 +73,7 @@ extern void FreeSnapshotBuilder(SnapBuild *builder); extern void SnapBuildSnapDecRefcount(Snapshot snap); extern Snapshot SnapBuildInitialSnapshot(SnapBuild *builder); +extern Snapshot SnapBuildMVCCFromHistoric(Snapshot snapshot, bool in_place); extern const char *SnapBuildExportSnapshot(SnapBuild *builder); extern void SnapBuildClearExportedSnapshot(void); extern void SnapBuildResetExportedSnapshotState(void); diff --git a/src/include/utils/snapmgr.h b/src/include/utils/snapmgr.h index d346be71642..147b190210a 100644 --- a/src/include/utils/snapmgr.h +++ b/src/include/utils/snapmgr.h @@ -60,6 +60,7 @@ extern Snapshot GetTransactionSnapshot(void); extern Snapshot GetLatestSnapshot(void); extern void SnapshotSetCommandId(CommandId curcid); +extern Snapshot CopySnapshot(Snapshot snapshot); extern Snapshot GetCatalogSnapshot(Oid relid); extern Snapshot GetNonHistoricCatalogSnapshot(Oid relid); extern void InvalidateCatalogSnapshot(void); -- 2.43.5 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v12-0003-Move-the-recheck-branch-to-a-separate-function.patch From 8584b47b926b8ba4f917db9af5337486228c81a1 Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Tue, 1 Apr 2025 13:48:57 +0200 Subject: [PATCH 3/9] Move the "recheck" branch to a separate function. At some point I thought that the relation must be unlocked during the call of setup_logical_decoding(), to avoid a deadlock. In that case we'd need to recheck afterwards if the table still meets the requirements of cluster_rel(). Eventually I concluded that the risk of that deadlock is not that high, so the table stays locked during the call of setup_logical_decoding(). Therefore the rechecking code is only executed once per table. Anyway, this patch might be useful in terms of code readability. --- src/backend/commands/cluster.c | 106 +++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 45 deletions(-) diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index 9ae3d87e412..67625d52f12 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -78,6 +78,8 @@ typedef struct static void cluster_multiple_rels(List *rtcs, ClusterParams *params, ClusterCommand cmd); +static bool cluster_rel_recheck(Relation OldHeap, Oid indexOid, Oid userid, + ClusterCommand cmd, int options); static void rebuild_relation(Relation OldHeap, Relation index, bool verbose, ClusterCommand cmd); static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, @@ -329,52 +331,9 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, * to cluster a not-previously-clustered index. */ if (recheck) - { - /* Check that the user still has privileges for the relation */ - if (!cluster_is_permitted_for_relation(tableOid, save_userid, cmd)) - { - relation_close(OldHeap, AccessExclusiveLock); + if (!cluster_rel_recheck(OldHeap, indexOid, save_userid, cmd, + params->options)) goto out; - } - - /* - * Silently skip a temp table for a remote session. Only doing this - * check in the "recheck" case is appropriate (which currently means - * somebody is executing a database-wide CLUSTER or on a partitioned - * table), because there is another check in cluster() which will stop - * any attempt to cluster remote temp tables by name. There is - * another check in cluster_rel which is redundant, but we leave it - * for extra safety. - */ - if (RELATION_IS_OTHER_TEMP(OldHeap)) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - - if (OidIsValid(indexOid)) - { - /* - * Check that the index still exists - */ - if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid))) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - - /* - * Check that the index is still the one with indisclustered set, - * if needed. - */ - if ((params->options & CLUOPT_RECHECK_ISCLUSTERED) != 0 && - !get_index_isclustered(indexOid)) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - } - } /* * We allow VACUUM FULL, but not CLUSTER, on shared catalogs. CLUSTER @@ -459,6 +418,63 @@ out: pgstat_progress_end_command(); } +/* + * Check if the table (and its index) still meets the requirements of + * cluster_rel(). + */ +static bool +cluster_rel_recheck(Relation OldHeap, Oid indexOid, Oid userid, + ClusterCommand cmd, int options) +{ + Oid tableOid = RelationGetRelid(OldHeap); + + /* Check that the user still has privileges for the relation */ + if (!cluster_is_permitted_for_relation(tableOid, userid, cmd)) + { + relation_close(OldHeap, AccessExclusiveLock); + return false; + } + + /* + * Silently skip a temp table for a remote session. Only doing this check + * in the "recheck" case is appropriate (which currently means somebody is + * executing a database-wide CLUSTER or on a partitioned table), because + * there is another check in cluster() which will stop any attempt to + * cluster remote temp tables by name. There is another check in + * cluster_rel which is redundant, but we leave it for extra safety. + */ + if (RELATION_IS_OTHER_TEMP(OldHeap)) + { + relation_close(OldHeap, AccessExclusiveLock); + return false; + } + + if (OidIsValid(indexOid)) + { + /* + * Check that the index still exists + */ + if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid))) + { + relation_close(OldHeap, AccessExclusiveLock); + return false; + } + + /* + * Check that the index is still the one with indisclustered set, if + * needed. + */ + if ((options & CLUOPT_RECHECK_ISCLUSTERED) != 0 && + !get_index_isclustered(indexOid)) + { + relation_close(OldHeap, AccessExclusiveLock); + return false; + } + } + + return true; +} + /* * Verify that the specified heap and index are valid to cluster on * -- 2.43.5 --=-=-= Content-Type: text/plain Content-Disposition: attachment; filename=v12-0004-Add-CONCURRENTLY-option-to-REPACK-command.patch From 8fc1631deb223682d60eaf18c87b0abc42db5214 Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Tue, 1 Apr 2025 13:48:57 +0200 Subject: [PATCH 4/9] 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. Since we cannot get stronger lock without releasing the weaker one first, we acquire the exclusive lock in the beginning and keep it till the end of the processing.) 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 write into it. First, we scan the relation using a "historic snapshot", and insert all the tuples satisfying this snapshot into the new file. Second, logical decoding is used to capture the data changes done by applications during the copying (i.e. changes that do not satisfy the historic snapshot mentioned above), and those are applied to the new file before we acquire the exclusive lock 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, before we swap the files.) 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. Like the existing implementation of REPACK, the variant with the CONCURRENTLY option also requires an extra space for the new relation and index files (which coexist with the old files for some time). In addition, the CONCURRENTLY option might introduce a lag in releasing WAL segments for archiving / recycling. This is due to the decoding of the data changes done by applications concurrently. When copying the table contents into the new file, we check the lag periodically. If it exceeds the size of WAL segment, we decode all the available WAL before resuming the copying. (Of course, the changes are not applied until the whole table contents is copied.) A background worker might be a better approach for the decoding - let's consider implementing it in the future. --- doc/src/sgml/monitoring.sgml | 65 +- doc/src/sgml/ref/repack.sgml | 116 +- src/Makefile | 1 + src/backend/access/heap/heapam_handler.c | 145 +- src/backend/access/heap/heapam_visibility.c | 30 +- src/backend/access/heap/rewriteheap.c | 6 +- src/backend/access/transam/xact.c | 11 +- src/backend/catalog/index.c | 43 +- src/backend/catalog/system_views.sql | 30 +- src/backend/commands/cluster.c | 1818 +++++++++++++++-- src/backend/commands/matview.c | 2 +- src/backend/commands/tablecmds.c | 1 + src/backend/commands/vacuum.c | 12 +- src/backend/meson.build | 1 + src/backend/parser/gram.y | 17 +- src/backend/replication/logical/decode.c | 24 + src/backend/replication/logical/snapbuild.c | 20 + .../replication/pgoutput_repack/Makefile | 32 + .../replication/pgoutput_repack/meson.build | 18 + .../pgoutput_repack/pgoutput_repack.c | 288 +++ src/backend/storage/ipc/ipci.c | 1 + .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/cache/relcache.c | 1 + src/backend/utils/time/snapmgr.c | 3 +- src/bin/psql/tab-complete.in.c | 25 +- src/include/access/heapam.h | 4 + src/include/access/tableam.h | 10 + src/include/catalog/index.h | 3 + src/include/commands/cluster.h | 87 +- src/include/commands/progress.h | 17 +- src/include/nodes/parsenodes.h | 1 + src/include/replication/snapbuild.h | 1 + src/include/storage/lockdefs.h | 4 +- src/include/storage/lwlocklist.h | 1 + src/include/utils/snapmgr.h | 2 + src/test/regress/expected/rules.out | 29 +- src/tools/pgindent/typedefs.list | 4 + 37 files changed, 2611 insertions(+), 263 deletions(-) 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 diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 0a6229c391a..e385a55272b 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -5835,14 +5835,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. @@ -6058,14 +6079,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. @@ -6146,6 +6188,13 @@ FROM pg_stat_get_backend_idset() AS backendid; REPACK is currently writing the new heap.
+ + catch-up + + REPACK is currently processing the DML commands that + other transactions executed during any of the preceding phase. + + swapping relation files diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml index 84f3c3e3f2b..9ee640e3517 100644 --- a/doc/src/sgml/ref/repack.sgml +++ b/doc/src/sgml/ref/repack.sgml @@ -22,6 +22,7 @@ PostgreSQL documentation REPACK [ ( option [, ...] ) ] [ table_name [ USING INDEXindex_name ] ] +REPACK [ ( option [, ...] ) ] CONCURRENTLY table_name [ USING INDEXindex_name ] where option can be one of: @@ -48,7 +49,8 @@ REPACK [ ( option [, ...] ) ] [ 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. @@ -61,7 +63,8 @@ REPACK [ ( option [, ...] ) ] [ 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. @@ -160,6 +163,115 @@ REPACK [ ( option [, ...] ) ] [ + + 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. + + + + Note that REPACK with the + 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 tuples from the old + file. Thus the tuples inserted into the old file during the copying are + also stored in separately in a temporary file, so they can eventually + be applied to the new file. + + + + Furthermore, the data changes performed during the copying are + extracted from write-ahead log (WAL), and + this extraction (decoding) only takes place when certain amount of WAL + has been written. Therefore, WAL removal can be delayed by this + threshold. Currently the threshold is equal to the value of + the wal_segment_size + configuration parameter. + + + + + The CONCURRENTLY option cannot be used in the + following cases: + + + + + The table is UNLOGGED. + + + + + + The table is partitioned. + + + + + + The table is a system catalog or a TOAST table. + + + + + + REPACK is executed inside a transaction block. + + + + + + The wal_level + configuration parameter is less than logical. + + + + + + The max_replication_slots + configuration parameter does not allow for creation of an additional + replication slot. + + + + + + + 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_handler.c b/src/backend/access/heap/heapam_handler.c index 18e349c3466..371afa6ad59 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -33,6 +33,7 @@ #include "catalog/index.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" +#include "commands/cluster.h" #include "commands/progress.h" #include "executor/executor.h" #include "miscadmin.h" @@ -53,6 +54,9 @@ static void reform_and_rewrite_tuple(HeapTuple tuple, static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, OffsetNumber tupoffset); +static HeapTuple accept_tuple_for_concurrent_copy(HeapTuple tuple, + Snapshot snapshot, + Buffer buffer); static BlockNumber heapam_scan_get_blocks_done(HeapScanDesc hscan); @@ -685,6 +689,8 @@ static void heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, Relation OldIndex, bool use_sort, TransactionId OldestXmin, + Snapshot snapshot, + LogicalDecodingContext *decoding_ctx, TransactionId *xid_cutoff, MultiXactId *multi_cutoff, double *num_tuples, @@ -705,6 +711,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool *isnull; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; + bool concurrent = snapshot != NULL; + XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL); /* Remember if it's a system catalog */ is_system_catalog = IsSystemRelation(OldHeap); @@ -783,8 +791,10 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, for (;;) { HeapTuple tuple; + bool tuple_copied = false; Buffer buf; bool isdead; + HTSV_Result vis; CHECK_FOR_INTERRUPTS(); @@ -839,7 +849,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, LockBuffer(buf, BUFFER_LOCK_SHARE); - switch (HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf)) + switch ((vis = HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf))) { case HEAPTUPLE_DEAD: /* Definitely dead */ @@ -855,14 +865,15 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, case HEAPTUPLE_INSERT_IN_PROGRESS: /* - * 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 + * 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. + * there. Also, there's no exclusive lock during concurrent + * processing. Give a warning if neither case applies; but in + * any case we had better copy it. */ - if (!is_system_catalog && + if (!is_system_catalog && !concurrent && !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data))) elog(WARNING, "concurrent insert in progress within table \"%s\"", RelationGetRelationName(OldHeap)); @@ -874,7 +885,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, /* * Similar situation to INSERT_IN_PROGRESS case. */ - if (!is_system_catalog && + if (!is_system_catalog && !concurrent && !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple->t_data))) elog(WARNING, "concurrent delete in progress within table \"%s\"", RelationGetRelationName(OldHeap)); @@ -888,8 +899,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, break; } - LockBuffer(buf, BUFFER_LOCK_UNLOCK); - if (isdead) { *tups_vacuumed += 1; @@ -900,9 +909,47 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, *tups_vacuumed += 1; *tups_recently_dead -= 1; } + + LockBuffer(buf, BUFFER_LOCK_UNLOCK); continue; } + if (concurrent) + { + /* + * Ignore concurrent changes now, they'll be processed later via + * logical decoding. + * + * INSERT_IN_PROGRESS is rejected right away because our snapshot + * should represent a point in time which should precede (or be + * equal to) the state of transactions as it was when the + * "SatisfiesVacuum" test was performed. Thus + * accept_tuple_for_concurrent_copy() should not consider the + * tuple inserted. + */ + if (vis == HEAPTUPLE_INSERT_IN_PROGRESS) + tuple = NULL; + else + tuple = accept_tuple_for_concurrent_copy(tuple, snapshot, + buf); + /* Tuple not suitable for the new heap? */ + if (tuple == NULL) + { + LockBuffer(buf, BUFFER_LOCK_UNLOCK); + continue; + } + + /* Remember that we have to free the tuple eventually. */ + tuple_copied = true; + } + + /* + * In the concurrent case, we have a copy of the tuple, so we don't + * worry whether the source tuple will be deleted / updated after we + * release the lock. + */ + LockBuffer(buf, BUFFER_LOCK_UNLOCK); + *num_tuples += 1; if (tuplesort != NULL) { @@ -919,7 +966,7 @@ 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]; @@ -934,6 +981,33 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, ct_val[1] = *num_tuples; pgstat_progress_update_multi_param(2, ct_index, ct_val); } + if (tuple_copied) + heap_freetuple(tuple); + + /* + * Process the WAL produced by the load, as well as by other + * transactions, so that the replication slot can advance and WAL does + * not pile up. Use wal_segment_size as a threshold so that we do not + * introduce the decoding overhead too often. + * + * Of course, we must not apply the changes until the initial load has + * completed. + * + * Note that our insertions into the new table should not be decoded + * as we (intentionally) do not write the logical decoding specific + * information to WAL. + */ + if (concurrent) + { + XLogRecPtr end_of_wal; + + end_of_wal = GetFlushRecPtr(NULL); + if ((end_of_wal - end_of_wal_prev) > wal_segment_size) + { + repack_decode_concurrent_changes(decoding_ctx, end_of_wal); + end_of_wal_prev = end_of_wal; + } + } } if (indexScan != NULL) @@ -977,7 +1051,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, values, isnull, rwstate); /* Report n_tuples */ - pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_WRITTEN, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } @@ -2023,6 +2097,53 @@ heapam_scan_get_blocks_done(HeapScanDesc hscan) return blocks_done; } +/* + * Return copy of 'tuple' if it has been inserted according to 'snapshot', or + * NULL if the insertion took place in the future. If the tuple is already + * marked as deleted or updated by a transaction that 'snapshot' still + * considers running, clear the deletion / update XID in the header of the + * copied tuple. This way the returned tuple is suitable for insertion into + * the new heap. + */ +static HeapTuple +accept_tuple_for_concurrent_copy(HeapTuple tuple, Snapshot snapshot, + Buffer buffer) +{ + HeapTuple result; + + Assert(snapshot->snapshot_type == SNAPSHOT_MVCC); + + /* + * First, check if the tuple insertion is visible by our snapshot. + */ + if (!HeapTupleMVCCInserted(tuple, snapshot, buffer)) + return NULL; + + result = heap_copytuple(tuple); + + /* + * If the tuple was deleted / updated but our snapshot still sees it, we + * need to keep it. In that case, clear the information that indicates the + * deletion / update. Otherwise the tuple chain would stay incomplete (as + * we will reject the new tuple above), and the delete / update would fail + * if executed later during logical decoding. + */ + if (TransactionIdIsNormal(HeapTupleHeaderGetRawXmax(result->t_data)) && + HeapTupleMVCCNotDeleted(result, snapshot, buffer)) + { + /* TODO More work needed here? */ + result->t_data->t_infomask |= HEAP_XMAX_INVALID; + HeapTupleHeaderSetXmax(result->t_data, 0); + } + + /* + * Accept the tuple even if our snapshot considers it deleted - older + * snapshots can still see the tuple, while the decoded transactions + * should not try to update / delete it again. + */ + return result; +} + /* ------------------------------------------------------------------------ * Miscellaneous callbacks for the heap AM diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c index 05f6946fe60..a46e1812b21 100644 --- a/src/backend/access/heap/heapam_visibility.c +++ b/src/backend/access/heap/heapam_visibility.c @@ -955,13 +955,14 @@ HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot, * did TransactionIdIsInProgress in each call --- to no avail, as long as the * inserting/deleting transaction was still running --- which was more cycles * and more contention on ProcArrayLock. + * + * The checks are split into two functions, HeapTupleMVCCInserted() and + * HeapTupleMVCCNotDeleted(), because they are also useful separately. */ static bool HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, Buffer buffer) { - HeapTupleHeader tuple = htup->t_data; - /* * Assert that the caller has registered the snapshot. This function * doesn't care about the registration as such, but in general you @@ -974,6 +975,20 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, Assert(ItemPointerIsValid(&htup->t_self)); Assert(htup->t_tableOid != InvalidOid); + return HeapTupleMVCCInserted(htup, snapshot, buffer) && + HeapTupleMVCCNotDeleted(htup, snapshot, buffer); +} + +/* + * HeapTupleMVCCInserted + * True iff heap tuple was successfully inserted for the given MVCC + * snapshot. + */ +bool +HeapTupleMVCCInserted(HeapTuple htup, Snapshot snapshot, Buffer buffer) +{ + HeapTupleHeader tuple = htup->t_data; + if (!HeapTupleHeaderXminCommitted(tuple)) { if (HeapTupleHeaderXminInvalid(tuple)) @@ -1082,6 +1097,17 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, } /* by here, the inserting transaction has committed */ + return true; +} + +/* + * HeapTupleMVCCNotDeleted + * True iff heap tuple was not deleted for the given MVCC snapshot. + */ +bool +HeapTupleMVCCNotDeleted(HeapTuple htup, Snapshot snapshot, Buffer buffer) +{ + HeapTupleHeader tuple = htup->t_data; if (tuple->t_infomask & HEAP_XMAX_INVALID) /* xid invalid or aborted */ return true; diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c index e6d2b5fced1..6aa2ed214f2 100644 --- a/src/backend/access/heap/rewriteheap.c +++ b/src/backend/access/heap/rewriteheap.c @@ -617,9 +617,9 @@ raw_heap_insert(RewriteState state, HeapTuple tup) int 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/access/transam/xact.c b/src/backend/access/transam/xact.c index b885513f765..23f2de587a1 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -215,6 +215,7 @@ typedef struct TransactionStateData bool parallelChildXact; /* is any parent transaction parallel? */ bool chain; /* start a new block after this one */ bool topXidLogged; /* for a subxact: is top-level XID logged? */ + bool internal; /* for a subxact: launched internally? */ struct TransactionStateData *parent; /* back link to parent */ } TransactionStateData; @@ -4723,6 +4724,7 @@ BeginInternalSubTransaction(const char *name) /* Normal subtransaction start */ PushTransaction(); s = CurrentTransactionState; /* changed by push */ + s->internal = true; /* * Savepoint names, like the TransactionState block itself, live @@ -5239,7 +5241,13 @@ AbortSubTransaction(void) LWLockReleaseAll(); pgstat_report_wait_end(); - pgstat_progress_end_command(); + + /* + * Internal subtransacion might be used by an user command, in which case + * the command outlives the subtransaction. + */ + if (!s->internal) + pgstat_progress_end_command(); pgaio_error_cleanup(); @@ -5456,6 +5464,7 @@ PushTransaction(void) s->parallelModeLevel = 0; s->parallelChildXact = (p->parallelModeLevel != 0 || p->parallelChildXact); s->topXidLogged = false; + s->internal = false; CurrentTransactionState = s; diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 466cf0fdef6..c70521d1d54 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -1418,22 +1418,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, for (int i = 0; i < newInfo->ii_NumIndexAttrs; i++) opclassOptions[i] = get_attoptions(oldIndexId, i + 1); - /* Extract statistic targets for each attribute */ - stattargets = palloc0_array(NullableDatum, newInfo->ii_NumIndexAttrs); - for (int i = 0; i < newInfo->ii_NumIndexAttrs; i++) - { - HeapTuple tp; - Datum dat; - - tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(oldIndexId), Int16GetDatum(i + 1)); - if (!HeapTupleIsValid(tp)) - elog(ERROR, "cache lookup failed for attribute %d of relation %u", - i + 1, oldIndexId); - dat = SysCacheGetAttr(ATTNUM, tp, Anum_pg_attribute_attstattarget, &isnull); - ReleaseSysCache(tp); - stattargets[i].value = dat; - stattargets[i].isnull = isnull; - } + stattargets = get_index_stattargets(oldIndexId, newInfo); /* * Now create the new index. @@ -1472,6 +1457,32 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, return newIndexId; } +NullableDatum * +get_index_stattargets(Oid indexid, IndexInfo *indInfo) +{ + NullableDatum *stattargets; + + /* Extract statistic targets for each attribute */ + stattargets = palloc0_array(NullableDatum, indInfo->ii_NumIndexAttrs); + for (int i = 0; i < indInfo->ii_NumIndexAttrs; i++) + { + HeapTuple tp; + Datum dat; + bool isnull; + + tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(indexid), Int16GetDatum(i + 1)); + if (!HeapTupleIsValid(tp)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + i + 1, indexid); + dat = SysCacheGetAttr(ATTNUM, tp, Anum_pg_attribute_attstattarget, &isnull); + ReleaseSysCache(tp); + stattargets[i].value = dat; + stattargets[i].isnull = isnull; + } + + return stattargets; +} + /* * index_concurrently_build * diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 5de46bcac52..70265e5e701 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1249,16 +1249,17 @@ CREATE VIEW pg_stat_progress_cluster 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' + -- 5 is 'catch-up', but that should not appear here. + 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 cluster_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.param8 AS heap_blks_total, + S.param9 AS heap_blks_scanned, + S.param10 AS index_rebuild_count FROM pg_stat_get_progress_info('CLUSTER') AS S LEFT JOIN pg_database D ON S.datid = D.oid; @@ -1275,16 +1276,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; diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index 67625d52f12..4d08a28ff7e 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -25,6 +25,10 @@ #include "access/toast_internals.h" #include "access/transam.h" #include "access/xact.h" +#include "access/xlog.h" +#include "access/xlog_internal.h" +#include "access/xloginsert.h" +#include "access/xlogutils.h" #include "catalog/catalog.h" #include "catalog/dependency.h" #include "catalog/heap.h" @@ -32,6 +36,7 @@ #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" +#include "catalog/pg_control.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" #include "commands/cluster.h" @@ -39,10 +44,15 @@ #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/vacuum.h" +#include "executor/executor.h" #include "miscadmin.h" #include "optimizer/optimizer.h" #include "pgstat.h" +#include "replication/decode.h" +#include "replication/logical.h" +#include "replication/snapbuild.h" #include "storage/bufmgr.h" +#include "storage/ipc.h" #include "storage/lmgr.h" #include "storage/predicate.h" #include "utils/acl.h" @@ -76,16 +86,46 @@ typedef struct ((cmd) == CLUSTER_COMMAND_REPACK ? \ "repack" : "vacuum")) +/* + * The following definitions are used for concurrent processing. + */ + +/* + * The locators are used to avoid logical decoding of data that we do not need + * for our table. + */ +RelFileLocator repacked_rel_locator = {.relNumber = InvalidOid}; +RelFileLocator repacked_rel_toast_locator = {.relNumber = InvalidOid}; + +/* + * Everything we need to call ExecInsertIndexTuples(). + */ +typedef struct IndexInsertState +{ + ResultRelInfo *rri; + EState *estate; + + Relation ident_index; +} IndexInsertState; + +/* The WAL segment being decoded. */ +static XLogSegNo repack_current_segment = 0; + static void cluster_multiple_rels(List *rtcs, ClusterParams *params, - ClusterCommand cmd); + ClusterCommand cmd, LOCKMODE lockmode, + bool isTopLevel); static bool cluster_rel_recheck(Relation OldHeap, Oid indexOid, Oid userid, - ClusterCommand cmd, int options); + ClusterCommand cmd, LOCKMODE lmode, + int options); +static void check_repack_concurrently_requirements(Relation rel); static void rebuild_relation(Relation OldHeap, Relation index, bool verbose, - ClusterCommand cmd); + ClusterCommand cmd, bool concurrent, Oid userid); static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, + Snapshot snapshot, LogicalDecodingContext *decoding_ctx, bool verbose, ClusterCommand cmd, bool *pSwapToastByContent, - TransactionId *pFreezeXid, MultiXactId *pCutoffMulti); + TransactionId *pFreezeXid, + MultiXactId *pCutoffMulti); static List *get_tables_to_cluster(MemoryContext cluster_context); static List *get_tables_to_repack(MemoryContext repack_context); static List *get_tables_to_cluster_partitioned(MemoryContext cluster_context, @@ -93,8 +133,53 @@ static List *get_tables_to_cluster_partitioned(MemoryContext cluster_context, ClusterCommand cmd); static bool cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd); +static void begin_concurrent_repack(Relation rel); +static void end_concurrent_repack(void); +static LogicalDecodingContext *setup_logical_decoding(Oid relid, + const char *slotname, + TupleDesc tupdesc); +static HeapTuple get_changed_tuple(char *change); +static void apply_concurrent_changes(RepackDecodingState *dstate, + Relation rel, ScanKey key, int nkeys, + IndexInsertState *iistate); +static void apply_concurrent_insert(Relation rel, ConcurrentChange *change, + HeapTuple tup, IndexInsertState *iistate, + TupleTableSlot *index_slot); +static void apply_concurrent_update(Relation rel, HeapTuple tup, + HeapTuple tup_target, + ConcurrentChange *change, + IndexInsertState *iistate, + TupleTableSlot *index_slot); +static void apply_concurrent_delete(Relation rel, HeapTuple tup_target, + ConcurrentChange *change); +static HeapTuple find_target_tuple(Relation rel, ScanKey key, int nkeys, + HeapTuple tup_key, + IndexInsertState *iistate, + TupleTableSlot *ident_slot, + IndexScanDesc *scan_p); +static void process_concurrent_changes(LogicalDecodingContext *ctx, + XLogRecPtr end_of_wal, + Relation rel_dst, + Relation rel_src, + ScanKey ident_key, + int ident_key_nentries, + IndexInsertState *iistate); +static IndexInsertState *get_index_insert_state(Relation relation, + Oid ident_index_id); +static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src, + int *nentries); +static void free_index_insert_state(IndexInsertState *iistate); +static void cleanup_logical_decoding(LogicalDecodingContext *ctx); +static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, + Relation cl_index, + LogicalDecodingContext *ctx, + bool swap_toast_by_content, + TransactionId frozenXid, + MultiXactId cutoffMulti); +static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static Relation process_single_relation(RangeVar *relation, char *indexname, - ClusterCommand cmd, + ClusterCommand cmd, LOCKMODE lockmode, + bool isTopLevel, ClusterParams *params, Oid *indexOid_p); @@ -153,8 +238,9 @@ cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel) if (stmt->relation != NULL) { rel = process_single_relation(stmt->relation, stmt->indexname, - CLUSTER_COMMAND_CLUSTER, ¶ms, - &indexOid); + CLUSTER_COMMAND_CLUSTER, + AccessExclusiveLock, isTopLevel, + ¶ms, &indexOid); if (rel == NULL) return; } @@ -204,7 +290,8 @@ cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel) } /* Do the job. */ - cluster_multiple_rels(rtcs, ¶ms, CLUSTER_COMMAND_CLUSTER); + cluster_multiple_rels(rtcs, ¶ms, CLUSTER_COMMAND_CLUSTER, + AccessExclusiveLock, isTopLevel); /* Start a new transaction for the cleanup work. */ StartTransactionCommand(); @@ -221,8 +308,8 @@ cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel) * return. */ static void -cluster_multiple_rels(List *rtcs, ClusterParams *params, - ClusterCommand cmd) +cluster_multiple_rels(List *rtcs, ClusterParams *params, ClusterCommand cmd, + LOCKMODE lockmode, bool isTopLevel) { ListCell *lc; @@ -242,10 +329,10 @@ cluster_multiple_rels(List *rtcs, ClusterParams *params, /* functions in indexes may want a snapshot set */ PushActiveSnapshot(GetTransactionSnapshot()); - rel = table_open(rtc->tableOid, AccessExclusiveLock); + rel = table_open(rtc->tableOid, lockmode); /* Process this table */ - cluster_rel(rel, rtc->indexOid, params, cmd); + cluster_rel(rel, rtc->indexOid, params, cmd, isTopLevel); /* cluster_rel closes the relation, but keeps lock */ PopActiveSnapshot(); @@ -269,12 +356,18 @@ cluster_multiple_rels(List *rtcs, ClusterParams *params, * instead of index order. This is the new implementation of VACUUM FULL, * and error messages should refer to the operation as VACUUM not CLUSTER. * + * 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 commands is being executed. REPACK should be the only * caller of this function in the future. */ void cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, - ClusterCommand cmd) + ClusterCommand cmd, bool isTopLevel) { Oid tableOid = RelationGetRelid(OldHeap); Oid save_userid; @@ -284,8 +377,34 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, bool recheck = ((params->options & CLUOPT_RECHECK) != 0); Relation index; const char *cmd_str = CLUSTER_COMMAND_STR(cmd); + bool concurrent = ((params->options & CLUOPT_CONCURRENT) != 0); + LOCKMODE lmode; + + /* + * Check that the correct lock is held. The lock mode is + * AccessExclusiveLock for normal processing and ShareUpdateExclusiveLock + * for concurrent processing (so that SELECT, INSERT, UPDATE and DELETE + * commands work, but cluster_rel() cannot be called concurrently for the + * same relation). + */ + lmode = !concurrent ? AccessExclusiveLock : ShareUpdateExclusiveLock; - Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false)); + /* There are specific requirements on concurrent processing. */ + if (concurrent) + { + /* + * Make sure we have no XID assigned, otherwise call of + * setup_logical_decoding() can cause a deadlock. + * + * The existence of transaction block actually does not imply that XID + * was already assigned, but it very likely is. We might want to check + * the result of GetCurrentTransactionIdIfAny() instead, but that + * would be less clear from user's perspective. + */ + PreventInTransactionBlock(isTopLevel, "REPACK CONCURRENTLY"); + + check_repack_concurrently_requirements(OldHeap); + } /* Check for user-requested abort. */ CHECK_FOR_INTERRUPTS(); @@ -331,7 +450,7 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, * to cluster a not-previously-clustered index. */ if (recheck) - if (!cluster_rel_recheck(OldHeap, indexOid, save_userid, cmd, + if (!cluster_rel_recheck(OldHeap, indexOid, save_userid, cmd, lmode, params->options)) goto out; @@ -350,6 +469,12 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot %s a shared catalog", cmd_str))); + /* + * 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. @@ -370,8 +495,7 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, if (OidIsValid(indexOid)) { /* verify the index is good and lock it */ - check_index_is_clusterable(OldHeap, indexOid, AccessExclusiveLock, - cmd); + check_index_is_clusterable(OldHeap, indexOid, lmode, cmd); /* also open it */ index = index_open(indexOid, NoLock); } @@ -388,7 +512,9 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, 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; } @@ -401,11 +527,35 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, * 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, cmd); + PG_TRY(); + { + /* + * For concurrent processing, make sure that our logical decoding + * ignores data changes of other tables than the one we are + * processing. + */ + if (concurrent) + begin_concurrent_repack(OldHeap); + + rebuild_relation(OldHeap, index, verbose, cmd, concurrent, + save_userid); + } + PG_FINALLY(); + { + if (concurrent) + end_concurrent_repack(); + } + PG_END_TRY(); + /* rebuild_relation closes OldHeap, and index if valid */ out: @@ -424,14 +574,14 @@ out: */ static bool cluster_rel_recheck(Relation OldHeap, Oid indexOid, Oid userid, - ClusterCommand cmd, int options) + ClusterCommand cmd, LOCKMODE lmode, int options) { Oid tableOid = RelationGetRelid(OldHeap); /* Check that the user still has privileges for the relation */ if (!cluster_is_permitted_for_relation(tableOid, userid, cmd)) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } @@ -445,7 +595,7 @@ cluster_rel_recheck(Relation OldHeap, Oid indexOid, Oid userid, */ if (RELATION_IS_OTHER_TEMP(OldHeap)) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } @@ -456,7 +606,7 @@ cluster_rel_recheck(Relation OldHeap, Oid indexOid, Oid userid, */ if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid))) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } @@ -467,7 +617,7 @@ cluster_rel_recheck(Relation OldHeap, Oid indexOid, Oid userid, if ((options & CLUOPT_RECHECK_ISCLUSTERED) != 0 && !get_index_isclustered(indexOid)) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } } @@ -611,19 +761,87 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal) table_close(pg_index, RowExclusiveLock); } +/* + * Check if the CONCURRENTLY option is legal for the relation. + */ +static void +check_repack_concurrently_requirements(Relation rel) +{ + 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)))); + + /* + * Identity index is not set if the replica identity is FULL, but PK might + * exist in such a case. + */ + 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))))); +} + /* * rebuild_relation: rebuild an existing relation in index or physical order * - * OldHeap: table to rebuild. + * 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. * - * 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. + * 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, - ClusterCommand cmd) + ClusterCommand cmd, bool concurrent, Oid userid) { Oid tableOid = RelationGetRelid(OldHeap); Oid accessMethod = OldHeap->rd_rel->relam; @@ -631,21 +849,61 @@ 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; + NameData slotname; + LogicalDecodingContext *ctx = NULL; + Snapshot snapshot = NULL; +#if USE_ASSERT_CHECKING + LOCKMODE lmode; + + lmode = !concurrent ? AccessExclusiveLock : ShareUpdateExclusiveLock; + + Assert(CheckRelationLockedByMe(OldHeap, lmode, false) && + (index == NULL || CheckRelationLockedByMe(index, lmode, false))); +#endif + + if (concurrent) + { + TupleDesc tupdesc; + + /* + * REPACK CONCURRENTLY is not allowed in a transaction block, so this + * should never fire. + */ + Assert(GetTopTransactionIdIfAny() == InvalidTransactionId); + + /* + * A single backend should not execute multiple REPACK commands at a + * time, so use PID to make the slot unique. + */ + snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid); + + tupdesc = CreateTupleDescCopy(RelationGetDescr(OldHeap)); + + /* + * Prepare to capture the concurrent data changes. + * + * Note that this call waits 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 its still eligible for REPACK + * CONCURRENTLY. + */ + ctx = setup_logical_decoding(tableOid, NameStr(slotname), tupdesc); - Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false) && - (index == NULL || CheckRelationLockedByMe(index, AccessExclusiveLock, false))); + snapshot = SnapBuildInitialSnapshotForRepack(ctx->snapshot_builder); + } - if (index) + if (index && cmd == CLUSTER_COMMAND_CLUSTER) /* Mark the correct index as clustered */ mark_index_clustered(OldHeap, RelationGetRelid(index), true); /* 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. @@ -661,30 +919,49 @@ 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, cmd, - &swap_toast_by_content, &frozenXid, &cutoffMulti); + copy_table_data(NewHeap, OldHeap, index, snapshot, ctx, verbose, + cmd, &swap_toast_by_content, &frozenXid, &cutoffMulti); + if (concurrent) + { + rebuild_relation_finish_concurrent(NewHeap, OldHeap, index, + ctx, swap_toast_by_content, + frozenXid, cutoffMulti); + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_FINAL_CLEANUP); + + /* Done with decoding. */ + FreeSnapshot(snapshot); + cleanup_logical_decoding(ctx); + ReplicationSlotRelease(); + ReplicationSlotDrop(NameStr(slotname), false); + } + 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 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); + /* + * 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, - frozenXid, cutoffMulti, - relpersistence); + /* + * 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, + frozenXid, cutoffMulti, + relpersistence); + } } @@ -819,14 +1096,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, - ClusterCommand cmd, bool *pSwapToastByContent, +copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, + Snapshot snapshot, LogicalDecodingContext *decoding_ctx, + bool verbose, ClusterCommand cmd, bool *pSwapToastByContent, TransactionId *pFreezeXid, MultiXactId *pCutoffMulti) { Relation relRelation; @@ -845,6 +1126,7 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb const char *cmd_str = CLUSTER_COMMAND_STR(cmd); PGRUsage ru0; char *nspname; + bool concurrent = snapshot != NULL; pg_rusage_init(&ru0); @@ -948,8 +1230,48 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb * provided, else plain seqscan. */ if (OldIndex != NULL && OldIndex->rd_rel->relam == BTREE_AM_OID) + { + ResourceOwner oldowner = NULL; + ResourceOwner resowner = NULL; + + /* + * In the CONCURRENT case, use a dedicated resource owner so we don't + * leave any additional locks behind us that we cannot release easily. + */ + if (concurrent) + { + Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, + false)); + Assert(CheckRelationLockedByMe(OldIndex, ShareUpdateExclusiveLock, + false)); + + resowner = ResourceOwnerCreate(CurrentResourceOwner, + "plan_cluster_use_sort"); + oldowner = CurrentResourceOwner; + CurrentResourceOwner = resowner; + } + use_sort = plan_cluster_use_sort(RelationGetRelid(OldHeap), RelationGetRelid(OldIndex)); + + if (concurrent) + { + CurrentResourceOwner = oldowner; + + /* + * We are primarily concerned about locks, but if the planner + * happened to allocate any other resources, we should release + * them too because we're going to delete the whole resowner. + */ + ResourceOwnerRelease(resowner, RESOURCE_RELEASE_BEFORE_LOCKS, + false, false); + ResourceOwnerRelease(resowner, RESOURCE_RELEASE_LOCKS, + false, false); + ResourceOwnerRelease(resowner, RESOURCE_RELEASE_AFTER_LOCKS, + false, false); + ResourceOwnerDelete(resowner); + } + } else use_sort = false; @@ -981,7 +1303,9 @@ 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, + decoding_ctx, + &cutoffs.FreezeLimit, &cutoffs.MultiXactCutoff, &num_tuples, &tups_vacuumed, &tups_recently_dead); @@ -990,7 +1314,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); @@ -1448,14 +1776,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 */ @@ -1481,39 +1808,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, @@ -1825,89 +2160,1253 @@ cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd) return false; } +#define REPL_PLUGIN_NAME "pgoutput_repack" + /* - * REPACK is intended to be a replacement of both CLUSTER and VACUUM FULL. + * Call this function before REPACK CONCURRENTLY starts to setup logical + * decoding. It makes sure that other users of the table put enough + * information into WAL. + * + * The point is that at various places we expect that the table we're + * processing is treated like a system catalog. For example, we need to be + * able to scan it using a "historic snapshot" anytime during the processing + * (as opposed to scanning only at the start point of the decoding, as logical + * replication does during initial table synchronization), in order to apply + * concurrent UPDATE / DELETE commands. + * + * Note that TOAST table needs no attention here as it's not scanned using + * historic snapshot. */ -void -repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) +static void +begin_concurrent_repack(Relation rel) { - ListCell *lc; - ClusterParams params = {0}; - bool verbose = false; - Relation rel = NULL; - Oid indexOid = InvalidOid; - MemoryContext repack_context; - List *rtcs; + Oid toastrelid; - /* Parse option list */ - foreach(lc, stmt->params) + /* Avoid logical decoding of other relations by this backend. */ + repacked_rel_locator = rel->rd_locator; + toastrelid = rel->rd_rel->reltoastrelid; + if (OidIsValid(toastrelid)) { - DefElem *opt = (DefElem *) lfirst(lc); + Relation toastrel; - if (strcmp(opt->defname, "verbose") == 0) - verbose = defGetBoolean(opt); - else - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("unrecognized REPACK option \"%s\"", - opt->defname), - parser_errposition(pstate, opt->location))); + /* Avoid logical decoding of other TOAST relations. */ + toastrel = table_open(toastrelid, AccessShareLock); + repacked_rel_toast_locator = toastrel->rd_locator; + table_close(toastrel, AccessShareLock); } +} - params.options = (verbose ? CLUOPT_VERBOSE : 0); +/* + * Call this when done with REPACK CONCURRENTLY. + */ +static void +end_concurrent_repack(void) +{ + /* + * Restore normal function of (future) logical decoding for this backend. + */ + repacked_rel_locator.relNumber = InvalidOid; + repacked_rel_toast_locator.relNumber = InvalidOid; +} - if (stmt->relation != NULL) - { - rel = process_single_relation(stmt->relation, stmt->indexname, - CLUSTER_COMMAND_REPACK, ¶ms, - &indexOid); - if (rel == NULL) - return; - } +/* + * 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 * +setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc) +{ + LogicalDecodingContext *ctx; + RepackDecodingState *dstate; /* - * 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. + * Check if we can use logical decoding. */ - PreventInTransactionBlock(isTopLevel, "REPACK"); + CheckSlotPermissions(); + CheckLogicalDecodingRequirements(); - /* Also, we need a memory context to hold our list of relations */ - repack_context = AllocSetContextCreate(PortalContext, - "Repack", - ALLOCSET_DEFAULT_SIZES); + /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */ + ReplicationSlotCreate(slotname, true, RS_TEMPORARY, false, false, false); - params.options |= CLUOPT_RECHECK; - if (rel != NULL) - { - Oid relid; - bool rel_is_index; + /* + * Neither prepare_write nor do_write callback nor update_progress is + * useful for us. + * + * Regarding the value of need_full_snapshot, we pass false because the + * table we are processing is present in RepackedRelsHash and therefore, + * regarding logical decoding, treated like a catalog. + */ + ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, + NIL, + false, + InvalidXLogRecPtr, + XL_ROUTINE(.page_read = read_local_xlog_page, + .segment_open = wal_segment_open, + .segment_close = wal_segment_close), + NULL, NULL, NULL); - Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + /* + * We don't have control on setting fast_forward, so at least check it. + */ + Assert(!ctx->fast_forward); - if (OidIsValid(indexOid)) - { - relid = indexOid; - rel_is_index = true; - } - else + DecodingContextFindStartpoint(ctx); + + /* Some WAL records should have been read. */ + Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr); + + XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment, + wal_segment_size); + + /* + * Setup structures to store decoded changes. + */ + dstate = palloc0(sizeof(RepackDecodingState)); + dstate->relid = relid; + dstate->tstore = tuplestore_begin_heap(false, false, + maintenance_work_mem); + + dstate->tupdesc = tupdesc; + + /* Initialize the descriptor to store the changes ... */ + dstate->tupdesc_change = CreateTemplateTupleDesc(1); + + TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0); + /* ... as well as the corresponding slot. */ + dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change, + &TTSOpsMinimalTuple); + + dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner, + "logical decoding"); + + ctx->output_writer_private = dstate; + return ctx; +} + +/* + * Retrieve tuple from ConcurrentChange structure. + * + * The input data starts with the structure but it might not be appropriately + * aligned. + */ +static HeapTuple +get_changed_tuple(char *change) +{ + HeapTupleData tup_data; + HeapTuple result; + char *src; + + /* + * Ensure alignment before accessing the fields. (This is why we can't use + * heap_copytuple() instead of this function.) + */ + src = change + offsetof(ConcurrentChange, tup_data); + memcpy(&tup_data, src, sizeof(HeapTupleData)); + + result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len); + memcpy(result, &tup_data, sizeof(HeapTupleData)); + result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE); + src = change + SizeOfConcurrentChange; + memcpy(result->t_data, src, result->t_len); + + return result; +} + +/* + * Decode logical changes from the WAL sequence up to end_of_wal. + */ +void +repack_decode_concurrent_changes(LogicalDecodingContext *ctx, + XLogRecPtr end_of_wal) +{ + RepackDecodingState *dstate; + ResourceOwner resowner_old; + + /* + * Invalidate the "present" cache before moving to "(recent) history". + */ + InvalidateSystemCaches(); + + dstate = (RepackDecodingState *) ctx->output_writer_private; + resowner_old = CurrentResourceOwner; + CurrentResourceOwner = dstate->resowner; + + PG_TRY(); + { + while (ctx->reader->EndRecPtr < end_of_wal) { - relid = RelationGetRelid(rel); - rel_is_index = false; - } - rtcs = get_tables_to_cluster_partitioned(repack_context, relid, - rel_is_index, - CLUSTER_COMMAND_REPACK); + XLogRecord *record; + XLogSegNo segno_new; + char *errm = NULL; + XLogRecPtr end_lsn; - /* close relation, releasing lock on parent table */ - table_close(rel, AccessExclusiveLock); + record = XLogReadRecord(ctx->reader, &errm); + if (errm) + elog(ERROR, "%s", errm); + + if (record != NULL) + LogicalDecodingProcessRecord(ctx, ctx->reader); + + /* + * If WAL segment boundary has been crossed, inform the decoding + * system that the catalog_xmin can advance. (We can confirm more + * often, but a filling a single WAL segment should not take much + * time.) + */ + 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; + } + + CHECK_FOR_INTERRUPTS(); + } + InvalidateSystemCaches(); + CurrentResourceOwner = resowner_old; } - else - rtcs = get_tables_to_repack(repack_context); + PG_CATCH(); + { + /* clear all timetravel entries */ + InvalidateSystemCaches(); + CurrentResourceOwner = resowner_old; + PG_RE_THROW(); + } + PG_END_TRY(); +} + +/* + * Apply changes that happened during the initial load. + * + * 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. + */ +static void +apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, + ScanKey key, int nkeys, IndexInsertState *iistate) +{ + TupleTableSlot *index_slot, + *ident_slot; + HeapTuple tup_old = NULL; + + if (dstate->nchanges == 0) + return; + + /* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */ + index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple); + + /* A slot to fetch tuples from identity index. */ + ident_slot = table_slot_create(rel, NULL); + + while (tuplestore_gettupleslot(dstate->tstore, true, false, + dstate->tsslot)) + { + bool shouldFree; + HeapTuple tup_change, + tup, + tup_exist; + char *change_raw, + *src; + ConcurrentChange change; + bool isnull[1]; + Datum values[1]; + + CHECK_FOR_INTERRUPTS(); + + /* Get the change from the single-column tuple. */ + tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree); + heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull); + Assert(!isnull[0]); + + /* Make sure we access aligned data. */ + change_raw = (char *) DatumGetByteaP(values[0]); + src = (char *) VARDATA(change_raw); + memcpy(&change, src, SizeOfConcurrentChange); + + /* TRUNCATE change contains no tuple, so process it separately. */ + if (change.kind == CHANGE_TRUNCATE) + { + /* + * All the things that ExecuteTruncateGuts() does (such as firing + * triggers or handling the DROP_CASCADE behavior) should have + * taken place on the source relation. Thus we only do the actual + * truncation of the new relation (and its indexes). + */ + heap_truncate_one_rel(rel); + + pfree(tup_change); + continue; + } + + /* + * Extract the tuple from the change. The tuple is copied here because + * it might be assigned to 'tup_old', in which case it needs to + * survive into the next iteration. + */ + tup = get_changed_tuple(src); + + if (change.kind == CHANGE_UPDATE_OLD) + { + Assert(tup_old == NULL); + tup_old = tup; + } + else if (change.kind == CHANGE_INSERT) + { + Assert(tup_old == NULL); + + apply_concurrent_insert(rel, &change, tup, iistate, index_slot); + + pfree(tup); + } + else if (change.kind == CHANGE_UPDATE_NEW || + change.kind == CHANGE_DELETE) + { + IndexScanDesc ind_scan = NULL; + HeapTuple tup_key; + + if (change.kind == CHANGE_UPDATE_NEW) + { + tup_key = tup_old != NULL ? tup_old : tup; + } + else + { + Assert(tup_old == NULL); + tup_key = tup; + } + + /* + * Find the tuple to be updated or deleted. + */ + tup_exist = find_target_tuple(rel, key, nkeys, tup_key, + iistate, ident_slot, &ind_scan); + if (tup_exist == NULL) + elog(ERROR, "Failed to find target tuple"); + + if (change.kind == CHANGE_UPDATE_NEW) + apply_concurrent_update(rel, tup, tup_exist, &change, iistate, + index_slot); + else + apply_concurrent_delete(rel, tup_exist, &change); + + if (tup_old != NULL) + { + pfree(tup_old); + tup_old = NULL; + } + + pfree(tup); + index_endscan(ind_scan); + } + else + elog(ERROR, "Unrecognized kind of change: %d", change.kind); + + /* If there's any change, make it visible to the next iteration. */ + if (change.kind != CHANGE_UPDATE_OLD) + { + CommandCounterIncrement(); + UpdateActiveSnapshotCommandId(); + } + + /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */ + Assert(shouldFree); + pfree(tup_change); + } + + tuplestore_clear(dstate->tstore); + dstate->nchanges = 0; + + /* Cleanup. */ + ExecDropSingleTupleTableSlot(index_slot); + ExecDropSingleTupleTableSlot(ident_slot); +} + +static void +apply_concurrent_insert(Relation rel, ConcurrentChange *change, HeapTuple tup, + IndexInsertState *iistate, TupleTableSlot *index_slot) +{ + List *recheck; + + + simple_heap_insert(rel, tup); + + /* + * Update indexes. + * + * In case functions in the index need the active snapshot and caller + * hasn't set one. + */ + ExecStoreHeapTuple(tup, index_slot, false); + recheck = ExecInsertIndexTuples(iistate->rri, + index_slot, + iistate->estate, + false, /* update */ + false, /* noDupErr */ + NULL, /* specConflict */ + NIL, /* arbiterIndexes */ + false /* onlySummarizing */ + ); + + /* + * If recheck is required, it must have been preformed on the source + * relation by now. (All the logical changes we process here are already + * committed.) + */ + list_free(recheck); + + pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, 1); +} + +static void +apply_concurrent_update(Relation rel, HeapTuple tup, HeapTuple tup_target, + ConcurrentChange *change, IndexInsertState *iistate, + TupleTableSlot *index_slot) +{ + List *recheck; + TU_UpdateIndexes update_indexes; + + /* + * Write the new tuple into the new heap. ('tup' gets the TID assigned + * here.) + */ + simple_heap_update(rel, &tup_target->t_self, tup, &update_indexes); + + ExecStoreHeapTuple(tup, index_slot, false); + + if (update_indexes != TU_None) + { + recheck = ExecInsertIndexTuples(iistate->rri, + index_slot, + iistate->estate, + true, /* update */ + false, /* noDupErr */ + NULL, /* specConflict */ + NIL, /* arbiterIndexes */ + /* onlySummarizing */ + update_indexes == TU_Summarizing); + list_free(recheck); + } + + pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_UPDATED, 1); +} + +static void +apply_concurrent_delete(Relation rel, HeapTuple tup_target, + ConcurrentChange *change) +{ + simple_heap_delete(rel, &tup_target->t_self); + + pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_DELETED, 1); +} + +/* + * Find the tuple to be updated or deleted. + * + * 'key' is a pre-initialized scan key, into which the function will put the + * key values. + * + * 'tup_key' is a tuple containing the key values for the scan. + * + * On exit,'*scan_p' contains the scan descriptor used. The caller must close + * it when he no longer needs the tuple returned. + */ +static HeapTuple +find_target_tuple(Relation rel, ScanKey key, int nkeys, HeapTuple tup_key, + IndexInsertState *iistate, + TupleTableSlot *ident_slot, IndexScanDesc *scan_p) +{ + IndexScanDesc scan; + Form_pg_index ident_form; + int2vector *ident_indkey; + HeapTuple result = NULL; + + /* XXX no instrumentation for now */ + scan = index_beginscan(rel, iistate->ident_index, GetActiveSnapshot(), + NULL, nkeys, 0); + *scan_p = scan; + index_rescan(scan, key, nkeys, NULL, 0); + + /* Info needed to retrieve key values from heap tuple. */ + ident_form = iistate->ident_index->rd_index; + ident_indkey = &ident_form->indkey; + + /* Use the incoming tuple to finalize the scan key. */ + for (int i = 0; i < scan->numberOfKeys; i++) + { + ScanKey entry; + bool isnull; + int16 attno_heap; + + entry = &scan->keyData[i]; + attno_heap = ident_indkey->values[i]; + entry->sk_argument = heap_getattr(tup_key, + attno_heap, + rel->rd_att, + &isnull); + Assert(!isnull); + } + if (index_getnext_slot(scan, ForwardScanDirection, ident_slot)) + { + bool shouldFree; + + result = ExecFetchSlotHeapTuple(ident_slot, false, &shouldFree); + /* TTSOpsBufferHeapTuple has .get_heap_tuple != NULL. */ + Assert(!shouldFree); + } + + return result; +} + +/* + * Decode and apply concurrent changes. + * + * Pass rel_src iff its reltoastrelid is needed. + */ +static void +process_concurrent_changes(LogicalDecodingContext *ctx, XLogRecPtr end_of_wal, + Relation rel_dst, Relation rel_src, ScanKey ident_key, + int ident_key_nentries, IndexInsertState *iistate) +{ + RepackDecodingState *dstate; + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_CATCH_UP); + + dstate = (RepackDecodingState *) ctx->output_writer_private; + + repack_decode_concurrent_changes(ctx, end_of_wal); + + if (dstate->nchanges == 0) + return; + + PG_TRY(); + { + /* + * Make sure that TOAST values can eventually be accessed via the old + * relation - see comment in copy_table_data(). + */ + if (rel_src) + rel_dst->rd_toastoid = rel_src->rd_rel->reltoastrelid; + + apply_concurrent_changes(dstate, rel_dst, ident_key, + ident_key_nentries, iistate); + } + PG_FINALLY(); + { + if (rel_src) + rel_dst->rd_toastoid = InvalidOid; + } + PG_END_TRY(); +} + +static IndexInsertState * +get_index_insert_state(Relation relation, Oid ident_index_id) +{ + EState *estate; + int i; + IndexInsertState *result; + + result = (IndexInsertState *) palloc0(sizeof(IndexInsertState)); + estate = CreateExecutorState(); + + result->rri = (ResultRelInfo *) palloc(sizeof(ResultRelInfo)); + InitResultRelInfo(result->rri, relation, 0, 0, 0); + ExecOpenIndices(result->rri, false); + + /* + * Find the relcache entry of the identity index so that we spend no extra + * effort to open / close it. + */ + for (i = 0; i < result->rri->ri_NumIndices; i++) + { + Relation ind_rel; + + ind_rel = result->rri->ri_IndexRelationDescs[i]; + if (ind_rel->rd_id == ident_index_id) + result->ident_index = ind_rel; + } + if (result->ident_index == NULL) + elog(ERROR, "Failed to open identity index"); + + /* Only initialize fields needed by ExecInsertIndexTuples(). */ + result->estate = estate; + + return result; +} + +/* + * Build scan key to process logical changes. + */ +static ScanKey +build_identity_key(Oid ident_idx_oid, Relation rel_src, int *nentries) +{ + Relation ident_idx_rel; + Form_pg_index ident_idx; + int n, + i; + ScanKey result; + + Assert(OidIsValid(ident_idx_oid)); + ident_idx_rel = index_open(ident_idx_oid, AccessShareLock); + ident_idx = ident_idx_rel->rd_index; + n = ident_idx->indnatts; + result = (ScanKey) palloc(sizeof(ScanKeyData) * n); + for (i = 0; i < n; i++) + { + ScanKey entry; + int16 relattno; + Form_pg_attribute att; + Oid opfamily, + opcintype, + opno, + opcode; + + entry = &result[i]; + relattno = ident_idx->indkey.values[i]; + if (relattno >= 1) + { + TupleDesc desc; + + desc = rel_src->rd_att; + att = TupleDescAttr(desc, relattno - 1); + } + else + elog(ERROR, "Unexpected attribute number %d in index", relattno); + + opfamily = ident_idx_rel->rd_opfamily[i]; + opcintype = ident_idx_rel->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 = att->attcollation; + } + index_close(ident_idx_rel, AccessShareLock); + + *nentries = n; + return result; +} + +static void +free_index_insert_state(IndexInsertState *iistate) +{ + ExecCloseIndices(iistate->rri); + FreeExecutorState(iistate->estate); + pfree(iistate->rri); + pfree(iistate); +} + +static void +cleanup_logical_decoding(LogicalDecodingContext *ctx) +{ + RepackDecodingState *dstate; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + + ExecDropSingleTupleTableSlot(dstate->tsslot); + FreeTupleDesc(dstate->tupdesc_change); + FreeTupleDesc(dstate->tupdesc); + tuplestore_end(dstate->tstore); + + FreeDecodingContext(ctx); +} + +/* + * 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, + Relation cl_index, + LogicalDecodingContext *ctx, + bool swap_toast_by_content, + 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_old, + ident_idx_new; + IndexInsertState *iistate; + ScanKey ident_key; + int ident_key_nentries; + XLogRecPtr wal_insert_ptr, + end_of_wal; + char dummy_rec_data = '\0'; + Relation *ind_refs, + *ind_refs_p; + int nind; + + /* Like in cluster_rel(). */ + lockmode_old = ShareUpdateExclusiveLock; + Assert(CheckRelationLockedByMe(OldHeap, lockmode_old, false)); + Assert(cl_index == NULL || + CheckRelationLockedByMe(cl_index, lockmode_old, false)); + /* This is expected from the caller. */ + Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false)); + + ident_idx_old = RelationGetReplicaIndex(OldHeap); + + /* + * 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. + */ + ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old); + + /* + * Processing shouldn't start w/o valid identity index. + */ + Assert(OidIsValid(ident_idx_old)); + + /* Find "identity index" on the new relation. */ + ident_idx_new = InvalidOid; + forboth(lc, ind_oids_old, lc2, ind_oids_new) + { + Oid ind_old = lfirst_oid(lc); + Oid ind_new = lfirst_oid(lc2); + + if (ident_idx_old == ind_old) + { + ident_idx_new = ind_new; + break; + } + } + if (!OidIsValid(ident_idx_new)) + + /* + * Should not happen, given our lock on the old relation. + */ + ereport(ERROR, + (errmsg("Identity index missing on the new relation"))); + + /* Executor state to update indexes. */ + iistate = get_index_insert_state(NewHeap, ident_idx_new); + + /* + * Build scan key that we'll use to look for rows to be updated / deleted + * during logical decoding. + */ + ident_key = build_identity_key(ident_idx_new, OldHeap, &ident_key_nentries); + + /* + * 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. + */ + wal_insert_ptr = GetInsertRecPtr(); + XLogFlush(wal_insert_ptr); + 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(ctx, end_of_wal, NewHeap, + swap_toast_by_content ? OldHeap : NULL, + ident_key, ident_key_nentries, iistate); + + /* + * Acquire AccessExclusiveLock on the table, its TOAST relation (if there + * is one), all its indexes, so that we can swap the files. + * + * Before that, unlock the index temporarily to avoid deadlock in case + * another transaction is trying to lock it while holding the lock on the + * table. + */ + if (cl_index) + { + index_close(cl_index, ShareUpdateExclusiveLock); + cl_index = NULL; + } + /* For the same reason, unlock TOAST relation. */ + if (OldHeap->rd_rel->reltoastrelid) + LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock); + /* Finally lock the table */ + 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 an array, to handle predicate locks below. + */ + ind_refs_p = ind_refs = palloc_array(Relation, list_length(ind_oids_old)); + nind = 0; + foreach(lc, ind_oids_old) + { + Oid ind_oid; + Relation index; + + ind_oid = lfirst_oid(lc); + index = index_open(ind_oid, AccessExclusiveLock); + *ind_refs_p = index; + ind_refs_p++; + nind++; + } + + /* + * In addition, 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); + /* The same for indexes. */ + for (int i = 0; i < nind; i++) + { + Relation index = ind_refs[i]; + + TransferPredicateLocksToHeapRelation(index); + + /* + * References to indexes on the old relation are not needed anymore, + * however locks stay till the end of the transaction. + */ + index_close(index, NoLock); + } + pfree(ind_refs); + + /* + * Flush anything we see in WAL, to make sure that all changes committed + * while we were waiting for the exclusive lock are available for + * decoding. This should not be necessary if all backends had + * synchronous_commit set, but we can't rely on this setting. + * + * Unfortunately, GetInsertRecPtr() may lag behind the actual insert + * position, and GetLastImportantRecPtr() points at the start of the last + * record rather than at the end. Thus the simplest way to determine the + * insert position is to insert a dummy record and use its LSN. + * + * XXX Consider using GetLastImportantRecPtr() and adding the size of the + * last record (plus the total size of all the page headers the record + * spans)? + */ + XLogBeginInsert(); + XLogRegisterData(&dummy_rec_data, 1); + wal_insert_ptr = XLogInsert(RM_XLOG_ID, XLOG_NOOP); + XLogFlush(wal_insert_ptr); + end_of_wal = GetFlushRecPtr(NULL); + + /* Apply the concurrent changes again. */ + process_concurrent_changes(ctx, end_of_wal, NewHeap, + swap_toast_by_content ? OldHeap : NULL, + ident_key, ident_key_nentries, iistate); + + /* 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]; + + /* Zero out possible results from swapped_relation_files */ + memset(mapped_tables, 0, sizeof(mapped_tables)); + + swap_relation_files(ind_old, ind_new, + (old_table_oid == RelationRelationId), + 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(mapped_tables[i] == 0); +#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.) */ + pfree(ident_key); + free_index_insert_state(iistate); + + /* + * 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, + swap_toast_by_content, + false, true, false, + frozenXid, cutoffMulti, + relpersistence); +} + +/* + * Build indexes on NewHeap according to those on OldHeap. + * + * OldIndexes is the list of index OIDs on OldHeap. + * + * A list of OIDs of 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) +{ + StringInfo ind_name; + ListCell *lc; + List *result = NIL; + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_REBUILD_INDEX); + + ind_name = makeStringInfo(); + + foreach(lc, OldIndexes) + { + Oid ind_oid, + ind_oid_new, + tbsp_oid; + Relation ind; + IndexInfo *ind_info; + int i, + heap_col_id; + List *colnames; + int16 indnatts; + Oid *collations, + *opclasses; + HeapTuple tup; + bool isnull; + Datum d; + oidvector *oidvec; + int2vector *int2vec; + size_t oid_arr_size; + size_t int2_arr_size; + int16 *indoptions; + text *reloptions = NULL; + bits16 flags; + Datum *opclassOptions; + NullableDatum *stattargets; + + ind_oid = lfirst_oid(lc); + ind = index_open(ind_oid, AccessShareLock); + ind_info = BuildIndexInfo(ind); + + tbsp_oid = ind->rd_rel->reltablespace; + + /* + * Index name really doesn't matter, we'll eventually use only their + * storage. Just make them unique within the table. + */ + resetStringInfo(ind_name); + appendStringInfo(ind_name, "ind_%d", + list_cell_number(OldIndexes, lc)); + + flags = 0; + if (ind->rd_index->indisprimary) + flags |= INDEX_CREATE_IS_PRIMARY; + + colnames = NIL; + indnatts = ind->rd_index->indnatts; + oid_arr_size = sizeof(Oid) * indnatts; + int2_arr_size = sizeof(int16) * indnatts; + + collations = (Oid *) palloc(oid_arr_size); + for (i = 0; i < indnatts; i++) + { + char *colname; + + heap_col_id = ind->rd_index->indkey.values[i]; + if (heap_col_id > 0) + { + Form_pg_attribute att; + + /* Normal attribute. */ + att = TupleDescAttr(OldHeap->rd_att, heap_col_id - 1); + colname = pstrdup(NameStr(att->attname)); + collations[i] = att->attcollation; + } + else if (heap_col_id == 0) + { + HeapTuple tuple; + Form_pg_attribute att; + + /* + * Expression column is not present in relcache. What we need + * here is an attribute of the *index* relation. + */ + tuple = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(ind_oid), + Int16GetDatum(i + 1)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, + "cache lookup failed for attribute %d of relation %u", + i + 1, ind_oid); + att = (Form_pg_attribute) GETSTRUCT(tuple); + colname = pstrdup(NameStr(att->attname)); + collations[i] = att->attcollation; + ReleaseSysCache(tuple); + } + else + elog(ERROR, "Unexpected column number: %d", + heap_col_id); + + colnames = lappend(colnames, colname); + } + + /* + * Special effort needed for variable length attributes of + * Form_pg_index. + */ + tup = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(ind_oid)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for index %u", ind_oid); + d = SysCacheGetAttr(INDEXRELID, tup, Anum_pg_index_indclass, &isnull); + Assert(!isnull); + oidvec = (oidvector *) DatumGetPointer(d); + opclasses = (Oid *) palloc(oid_arr_size); + memcpy(opclasses, oidvec->values, oid_arr_size); + + d = SysCacheGetAttr(INDEXRELID, tup, Anum_pg_index_indoption, + &isnull); + Assert(!isnull); + int2vec = (int2vector *) DatumGetPointer(d); + indoptions = (int16 *) palloc(int2_arr_size); + memcpy(indoptions, int2vec->values, int2_arr_size); + ReleaseSysCache(tup); + + tup = SearchSysCache1(RELOID, ObjectIdGetDatum(ind_oid)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for index relation %u", ind_oid); + d = SysCacheGetAttr(RELOID, tup, Anum_pg_class_reloptions, &isnull); + reloptions = !isnull ? DatumGetTextPCopy(d) : NULL; + ReleaseSysCache(tup); + + opclassOptions = palloc0(sizeof(Datum) * ind_info->ii_NumIndexAttrs); + for (i = 0; i < ind_info->ii_NumIndexAttrs; i++) + opclassOptions[i] = get_attoptions(ind_oid, i + 1); + + stattargets = get_index_stattargets(ind_oid, ind_info); + + /* + * Neither parentIndexRelid nor parentConstraintId needs to be passed + * since the new catalog entries (pg_constraint, pg_inherits) would + * eventually be dropped. Therefore there's no need to record valid + * dependency on parents. + */ + ind_oid_new = index_create(NewHeap, + ind_name->data, + InvalidOid, + InvalidOid, /* parentIndexRelid */ + InvalidOid, /* parentConstraintId */ + InvalidOid, + ind_info, + colnames, + ind->rd_rel->relam, + tbsp_oid, + collations, + opclasses, + opclassOptions, + indoptions, + stattargets, + PointerGetDatum(reloptions), + flags, /* flags */ + 0, /* constr_flags */ + false, /* allow_system_table_mods */ + false, /* is_internal */ + NULL /* constraintId */ + ); + result = lappend_oid(result, ind_oid_new); + + index_close(ind, AccessShareLock); + list_free_deep(colnames); + pfree(collations); + pfree(opclasses); + pfree(indoptions); + if (reloptions) + pfree(reloptions); + } + + return result; +} + +/* + * REPACK is intended to be a replacement of both CLUSTER and VACUUM FULL. + */ +void +repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) +{ + ListCell *lc; + ClusterParams params = {0}; + bool verbose = false; + Relation rel = NULL; + Oid indexOid = InvalidOid; + MemoryContext repack_context; + List *rtcs; + LOCKMODE lockmode; + + /* Parse option list */ + foreach(lc, stmt->params) + { + DefElem *opt = (DefElem *) lfirst(lc); + + if (strcmp(opt->defname, "verbose") == 0) + verbose = defGetBoolean(opt); + else + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unrecognized REPACK option \"%s\"", + opt->defname), + parser_errposition(pstate, opt->location))); + } + + params.options = + (verbose ? CLUOPT_VERBOSE : 0) | + (stmt->concurrent ? CLUOPT_CONCURRENT : 0); + + /* + * Determine the lock mode expected by cluster_rel(). + * + * In the exclusive case, we obtain AccessExclusiveLock right away to + * avoid lock-upgrade hazard in the single-transaction case. In the + * CONCURRENTLY case, the AccessExclusiveLock will only be used at the end + * of processing, supposedly for very short time. Until then, we'll have + * to unlock the relation temporarily, so there's no lock-upgrade hazard. + */ + lockmode = (params.options & CLUOPT_CONCURRENT) == 0 ? + AccessExclusiveLock : ShareUpdateExclusiveLock; + + if (stmt->relation != NULL) + { + rel = process_single_relation(stmt->relation, stmt->indexname, + CLUSTER_COMMAND_REPACK, lockmode, + isTopLevel, ¶ms, &indexOid); + if (rel == NULL) + return; + } + + /* + * 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, "REPACK"); + + /* Also, we need a memory context to hold our list of relations */ + repack_context = AllocSetContextCreate(PortalContext, + "Repack", + ALLOCSET_DEFAULT_SIZES); + + params.options |= CLUOPT_RECHECK; + if (rel != NULL) + { + Oid relid; + bool rel_is_index; + + Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + /* See the ereport() above. */ + Assert((params.options & CLUOPT_CONCURRENT) == 0); + + if (OidIsValid(indexOid)) + { + relid = indexOid; + rel_is_index = true; + } + else + { + relid = RelationGetRelid(rel); + rel_is_index = false; + } + rtcs = get_tables_to_cluster_partitioned(repack_context, relid, + rel_is_index, + CLUSTER_COMMAND_REPACK); + + /* close relation, releasing lock on parent table */ + table_close(rel, lockmode); + } + else + rtcs = get_tables_to_repack(repack_context); + + /* Do the job. */ + cluster_multiple_rels(rtcs, ¶ms, CLUSTER_COMMAND_REPACK, lockmode, + isTopLevel); - /* Do the job. */ - cluster_multiple_rels(rtcs, ¶ms, CLUSTER_COMMAND_REPACK); /* Start a new transaction for the cleanup work. */ StartTransactionCommand(); @@ -1925,7 +3424,8 @@ repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) */ static Relation process_single_relation(RangeVar *relation, char *indexname, - ClusterCommand cmd, ClusterParams *params, + ClusterCommand cmd, LOCKMODE lockmode, + bool isTopLevel, ClusterParams *params, Oid *indexOid_p) { Relation rel; @@ -1935,12 +3435,10 @@ process_single_relation(RangeVar *relation, char *indexname, Oid tableOid; /* - * 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(relation, - AccessExclusiveLock, + lockmode, 0, RangeVarCallbackMaintainsTable, NULL); @@ -1994,7 +3492,7 @@ process_single_relation(RangeVar *relation, char *indexname, /* For non-partitioned tables, do what we came here to do. */ if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) { - cluster_rel(rel, indexOid, params, cmd); + cluster_rel(rel, indexOid, params, cmd, isTopLevel); /* cluster_rel closes the relation, but keeps lock */ return NULL; diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index e7854add178..df879c2a18d 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -904,7 +904,7 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner, static void refresh_by_heap_swap(Oid matviewOid, Oid OIDNewHeap, char relpersistence) { - finish_heap_swap(matviewOid, OIDNewHeap, false, false, true, true, + finish_heap_swap(matviewOid, OIDNewHeap, false, false, true, true, true, RecentXmin, ReadNextMultiXactId(), relpersistence); } diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index b7a74f25785..2b15e5b1505 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -5970,6 +5970,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode, finish_heap_swap(tab->relid, OIDNewHeap, false, false, true, !OidIsValid(tab->newTableSpace), + true, RecentXmin, ReadNextMultiXactId(), persistence); diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index a4ad23448f8..f9f8f5ebb58 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -124,7 +124,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); @@ -634,7 +634,8 @@ vacuum(List *relations, VacuumParams *params, BufferAccessStrategy bstrategy, 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; } @@ -1996,7 +1997,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; @@ -2264,7 +2265,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, /* VACUUM FULL is now a variant of CLUSTER; see cluster.c */ cluster_rel(rel, InvalidOid, &cluster_params, - CLUSTER_COMMAND_VACUUM); + CLUSTER_COMMAND_VACUUM, isTopLevel); /* cluster_rel closes the relation, but keeps lock */ rel = NULL; @@ -2310,7 +2311,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/meson.build b/src/backend/meson.build index 2b0db214804..50aa385a581 100644 --- a/src/backend/meson.build +++ b/src/backend/meson.build @@ -194,5 +194,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/parser/gram.y b/src/backend/parser/gram.y index 9c79265a438..634d0768851 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -11892,27 +11892,30 @@ cluster_index_specification: * * QUERY: * REPACK [ (options) ] [ [ USING INDEX ] ] + * REPACK [ (options) ] CONCURRENTLY [ USING INDEX ] * *****************************************************************************/ RepackStmt: - REPACK qualified_name repack_index_specification + REPACK opt_concurrently qualified_name repack_index_specification { RepackStmt *n = makeNode(RepackStmt); - n->relation = $2; - n->indexname = $3; + n->concurrent = $2; + n->relation = $3; + n->indexname = $4; n->params = NIL; $$ = (Node *) n; } - | REPACK '(' utility_option_list ')' qualified_name repack_index_specification + | REPACK '(' utility_option_list ')' opt_concurrently qualified_name repack_index_specification { RepackStmt *n = makeNode(RepackStmt); - n->relation = $5; - n->indexname = $6; n->params = $3; + n->concurrent = $5; + n->relation = $6; + n->indexname = $7; $$ = (Node *) n; } @@ -11923,6 +11926,7 @@ RepackStmt: n->relation = NULL; n->indexname = NULL; n->params = NIL; + n->concurrent = false; $$ = (Node *) n; } @@ -11933,6 +11937,7 @@ RepackStmt: n->relation = NULL; n->indexname = NULL; n->params = $3; + n->concurrent = false; $$ = (Node *) n; } ; diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c index 78f9a0a11c4..00f7bbc5f59 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/cluster.h" #include "replication/decode.h" #include "replication/logical.h" #include "replication/message.h" @@ -467,6 +468,29 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) TransactionId xid = XLogRecGetXid(buf->record); SnapBuild *builder = ctx->snapshot_builder; + /* + * Check if REPACK CONCURRENTLY is being performed by this backend. If so, + * only decode data changes of the table that it is processing, and the + * changes of its TOAST relation. + * + * (TOAST locator should not be set unless the main is.) + */ + Assert(!OidIsValid(repacked_rel_toast_locator.relNumber) || + OidIsValid(repacked_rel_locator.relNumber)); + + if (OidIsValid(repacked_rel_locator.relNumber)) + { + XLogReaderState *r = buf->record; + RelFileLocator locator; + + /* Not all records contain the block. */ + if (XLogRecGetBlockTagExtended(r, 0, &locator, NULL, NULL, NULL) && + !RelFileLocatorEquals(locator, repacked_rel_locator) && + (!OidIsValid(repacked_rel_toast_locator.relNumber) || + !RelFileLocatorEquals(locator, repacked_rel_toast_locator))) + return; + } + ReorderBufferProcessXid(ctx->reorder, xid, buf->origptr); /* diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c index e5d2a583ce6..c32e459411b 100644 --- a/src/backend/replication/logical/snapbuild.c +++ b/src/backend/replication/logical/snapbuild.c @@ -486,6 +486,26 @@ SnapBuildInitialSnapshot(SnapBuild *builder) return SnapBuildMVCCFromHistoric(snap, true); } +/* + * Build an MVCC snapshot for the initial data load performed by REPACK + * CONCURRENTLY command. + * + * The snapshot will only be used to scan one particular relation, which is + * treated like a catalog (therefore ->building_full_snapshot is not + * important), and the caller should already have a replication slot setup (so + * we do not set MyProc->xmin). XXX Do we yet need to add some restrictions? + */ +Snapshot +SnapBuildInitialSnapshotForRepack(SnapBuild *builder) +{ + Snapshot snap; + + Assert(builder->state == SNAPBUILD_CONSISTENT); + + snap = SnapBuildBuildSnapshot(builder); + return SnapBuildMVCCFromHistoric(snap, false); +} + /* * Turn a historic MVCC snapshot into an ordinary MVCC snapshot. * 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..133e865a4a0 --- /dev/null +++ b/src/backend/replication/pgoutput_repack/meson.build @@ -0,0 +1,18 @@ +# Copyright (c) 2022-2024, 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..687fbbc59bb --- /dev/null +++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c @@ -0,0 +1,288 @@ +/*------------------------------------------------------------------------- + * + * pgoutput_cluster.c + * Logical Replication output plugin for REPACK command + * + * Copyright (c) 2012-2024, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/replication/pgoutput_cluster/pgoutput_cluster.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/heaptoast.h" +#include "commands/cluster.h" +#include "replication/snapbuild.h" + +PG_MODULE_MAGIC; + +static void plugin_startup(LogicalDecodingContext *ctx, + OutputPluginOptions *opt, bool is_init); +static void plugin_shutdown(LogicalDecodingContext *ctx); +static void plugin_begin_txn(LogicalDecodingContext *ctx, + ReorderBufferTXN *txn); +static void plugin_commit_txn(LogicalDecodingContext *ctx, + ReorderBufferTXN *txn, XLogRecPtr commit_lsn); +static void plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, + Relation rel, ReorderBufferChange *change); +static void plugin_truncate(struct LogicalDecodingContext *ctx, + ReorderBufferTXN *txn, int nrelations, + Relation relations[], + ReorderBufferChange *change); +static void store_change(LogicalDecodingContext *ctx, + ConcurrentChangeKind kind, HeapTuple tuple); + +void +_PG_output_plugin_init(OutputPluginCallbacks *cb) +{ + AssertVariableIsOfType(&_PG_output_plugin_init, LogicalOutputPluginInit); + + cb->startup_cb = plugin_startup; + cb->begin_cb = plugin_begin_txn; + cb->change_cb = plugin_change; + cb->truncate_cb = plugin_truncate; + cb->commit_cb = plugin_commit_txn; + cb->shutdown_cb = plugin_shutdown; +} + + +/* initialize this plugin */ +static void +plugin_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 +plugin_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 +plugin_begin_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn) +{ +} + +/* COMMIT callback */ +static void +plugin_commit_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, + XLogRecPtr commit_lsn) +{ +} + +/* + * Callback for individual changed tuples + */ +static void +plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, + Relation relation, ReorderBufferChange *change) +{ + RepackDecodingState *dstate; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + + /* Only interested in one particular relation. */ + if (relation->rd_id != dstate->relid) + return; + + /* Decode entry depending on its type */ + switch (change->action) + { + case REORDER_BUFFER_CHANGE_INSERT: + { + HeapTuple newtuple; + + newtuple = change->data.tp.newtuple != NULL ? + change->data.tp.newtuple : NULL; + + /* + * Identity checks in the main function should have made this + * impossible. + */ + if (newtuple == NULL) + elog(ERROR, "Incomplete insert info."); + + store_change(ctx, CHANGE_INSERT, newtuple); + } + break; + case REORDER_BUFFER_CHANGE_UPDATE: + { + HeapTuple oldtuple, + newtuple; + + oldtuple = change->data.tp.oldtuple != NULL ? + change->data.tp.oldtuple : NULL; + newtuple = change->data.tp.newtuple != NULL ? + change->data.tp.newtuple : NULL; + + if (newtuple == NULL) + elog(ERROR, "Incomplete update info."); + + if (oldtuple != NULL) + store_change(ctx, CHANGE_UPDATE_OLD, oldtuple); + + store_change(ctx, CHANGE_UPDATE_NEW, newtuple); + } + break; + case REORDER_BUFFER_CHANGE_DELETE: + { + HeapTuple oldtuple; + + oldtuple = change->data.tp.oldtuple ? + change->data.tp.oldtuple : NULL; + + if (oldtuple == NULL) + elog(ERROR, "Incomplete delete info."); + + store_change(ctx, CHANGE_DELETE, oldtuple); + } + break; + default: + /* Should not come here */ + Assert(false); + break; + } +} + +static void +plugin_truncate(struct LogicalDecodingContext *ctx, ReorderBufferTXN *txn, + int nrelations, Relation relations[], + ReorderBufferChange *change) +{ + RepackDecodingState *dstate; + int i; + Relation relation = NULL; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + + /* Find the relation we are processing. */ + for (i = 0; i < nrelations; i++) + { + relation = relations[i]; + + if (RelationGetRelid(relation) == dstate->relid) + break; + } + + /* Is this truncation of another relation? */ + if (i == nrelations) + return; + + store_change(ctx, CHANGE_TRUNCATE, NULL); +} + +/* Store concurrent data change. */ +static void +store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind, + HeapTuple tuple) +{ + RepackDecodingState *dstate; + char *change_raw; + ConcurrentChange change; + bool flattened = false; + Size size; + Datum values[1]; + bool isnull[1]; + char *dst, + *dst_start; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + + size = MAXALIGN(VARHDRSZ) + SizeOfConcurrentChange; + + if (tuple) + { + /* + * ReorderBufferCommit() stores the TOAST chunks in its private memory + * context and frees them after having called apply_change(). + * Therefore we need flat copy (including TOAST) that we eventually + * copy into the memory context which is available to + * decode_concurrent_changes(). + */ + if (HeapTupleHasExternal(tuple)) + { + /* + * toast_flatten_tuple_to_datum() might be more convenient but we + * don't want the decompression it does. + */ + tuple = toast_flatten_tuple(tuple, dstate->tupdesc); + flattened = true; + } + + size += tuple->t_len; + } + + /* XXX Isn't there any function / macro to do this? */ + if (size >= 0x3FFFFFFF) + elog(ERROR, "Change is too big."); + + /* Construct the change. */ + change_raw = (char *) palloc0(size); + SET_VARSIZE(change_raw, size); + + /* + * Since the varlena alignment might not be sufficient for the structure, + * set the fields in a local instance and remember where it should + * eventually be copied. + */ + change.kind = kind; + dst_start = (char *) VARDATA(change_raw); + + /* No other information is needed for TRUNCATE. */ + if (change.kind == CHANGE_TRUNCATE) + { + memcpy(dst_start, &change, SizeOfConcurrentChange); + goto store; + } + + /* + * Copy the tuple. + * + * CAUTION: change->tup_data.t_data must be fixed on retrieval! + */ + memcpy(&change.tup_data, tuple, sizeof(HeapTupleData)); + dst = dst_start + SizeOfConcurrentChange; + memcpy(dst, tuple->t_data, tuple->t_len); + + /* The data has been copied. */ + if (flattened) + pfree(tuple); + +store: + /* Copy the structure so it can be stored. */ + memcpy(dst_start, &change, SizeOfConcurrentChange); + + /* Store as tuple of 1 bytea column. */ + values[0] = PointerGetDatum(change_raw); + isnull[0] = false; + tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change, + values, isnull); + + /* Accounting. */ + dstate->nchanges++; + + /* Cleanup. */ + pfree(change_raw); +} diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..e9ddf39500c 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -25,6 +25,7 @@ #include "access/xlogprefetcher.h" #include "access/xlogrecovery.h" #include "commands/async.h" +#include "commands/cluster.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/autovacuum.h" diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 4f44648aca8..1ee069c34ee 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -351,6 +351,7 @@ DSMRegistry "Waiting to read or update the dynamic shared memory registry." InjectionPoint "Waiting to read or update information related to injection points." SerialControl "Waiting to read or update shared pg_serial state." AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." +RepackedRels "Waiting to read or update information on tables being repacked concurrently." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 9f54a9e72b7..a495f22876d 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -64,6 +64,7 @@ #include "catalog/pg_type.h" #include "catalog/schemapg.h" #include "catalog/storage.h" +#include "commands/cluster.h" #include "commands/policy.h" #include "commands/publicationcmds.h" #include "commands/trigger.h" diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c index 70a6b8902d1..7f1c220e00b 100644 --- a/src/backend/utils/time/snapmgr.c +++ b/src/backend/utils/time/snapmgr.c @@ -213,7 +213,6 @@ static List *exportedSnapshots = NIL; /* Prototypes for local functions */ static void UnregisterSnapshotNoOwner(Snapshot snapshot); -static void FreeSnapshot(Snapshot snapshot); static void SnapshotResetXmin(void); /* ResourceOwner callbacks to track snapshot references */ @@ -646,7 +645,7 @@ CopySnapshot(Snapshot snapshot) * FreeSnapshot * Free the memory associated with a snapshot. */ -static void +void FreeSnapshot(Snapshot snapshot) { Assert(snapshot->regd_count == 0); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 31271786f21..a22e6cb6ccc 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4914,18 +4914,27 @@ match_previous_words(int pattern_id, } /* REPACK */ - else if (Matches("REPACK")) + else if (Matches("REPACK") || Matches("REPACK", "(*)")) + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_clusterables, + "CONCURRENTLY"); + else if (Matches("REPACK", "CONCURRENTLY")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables); - else if (Matches("REPACK", "(*)")) + else if (Matches("REPACK", "(*)", "CONCURRENTLY")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_clusterables); - /* If we have REPACK , then add "USING INDEX" */ - else if (Matches("REPACK", MatchAnyExcept("("))) + /* If we have REPACK [ CONCURRENTLY ] , then add "USING INDEX" */ + else if (Matches("REPACK", MatchAnyExcept("(|CONCURRENTLY")) || + Matches("REPACK", "CONCURRENTLY", MatchAnyExcept("("))) COMPLETE_WITH("USING INDEX"); - /* If we have REPACK (*) , then add "USING INDEX" */ - else if (Matches("REPACK", "(*)", MatchAny)) + /* If we have REPACK (*) [ CONCURRENTLY ] , then add "USING INDEX" */ + else if (Matches("REPACK", "(*)", MatchAnyExcept("CONCURRENTLY")) || + Matches("REPACK", "(*)", "CONCURRENTLY", MatchAnyExcept("("))) COMPLETE_WITH("USING INDEX"); - /* If we have REPACK USING, then add the index as well */ - else if (Matches("REPACK", MatchAny, "USING", "INDEX")) + + /* + * Complete ... [ (*) ] [ CONCURRENTLY ] USING INDEX, with a list of + * indexes for . + */ + else if (TailMatches(MatchAnyExcept("(|CONCURRENTLY"), "USING", "INDEX")) { set_completion_reference(prev3_wd); COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table); diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 1640d9c32f7..bdeb2f83540 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -421,6 +421,10 @@ extern HTSV_Result HeapTupleSatisfiesVacuumHorizon(HeapTuple htup, Buffer buffer TransactionId *dead_after); extern void HeapTupleSetHintBits(HeapTupleHeader tuple, Buffer buffer, uint16 infomask, TransactionId xid); +extern bool HeapTupleMVCCInserted(HeapTuple htup, Snapshot snapshot, + Buffer buffer); +extern bool HeapTupleMVCCNotDeleted(HeapTuple htup, Snapshot snapshot, + Buffer buffer); extern bool HeapTupleHeaderIsOnlyLocked(HeapTupleHeader tuple); extern bool HeapTupleIsSurelyDead(HeapTuple htup, struct GlobalVisState *vistest); diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h index b8cb1e744ad..b1ca73d6ea5 100644 --- a/src/include/access/tableam.h +++ b/src/include/access/tableam.h @@ -21,6 +21,7 @@ #include "access/sdir.h" #include "access/xact.h" #include "executor/tuptable.h" +#include "replication/logical.h" #include "storage/read_stream.h" #include "utils/rel.h" #include "utils/snapshot.h" @@ -630,6 +631,8 @@ typedef struct TableAmRoutine Relation OldIndex, bool use_sort, TransactionId OldestXmin, + Snapshot snapshot, + LogicalDecodingContext *decoding_ctx, TransactionId *xid_cutoff, MultiXactId *multi_cutoff, double *num_tuples, @@ -1637,6 +1640,10 @@ 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. + * - decoding_ctx - logical decoding context, to capture concurrent data + * changes. * * Output parameters: * - *xid_cutoff - rel's new relfrozenxid value, may be invalid @@ -1649,6 +1656,8 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable, Relation OldIndex, bool use_sort, TransactionId OldestXmin, + Snapshot snapshot, + LogicalDecodingContext *decoding_ctx, TransactionId *xid_cutoff, MultiXactId *multi_cutoff, double *num_tuples, @@ -1657,6 +1666,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable, { OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex, use_sort, OldestXmin, + snapshot, decoding_ctx, xid_cutoff, multi_cutoff, num_tuples, tups_vacuumed, tups_recently_dead); diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h index 4daa8bef5ee..66431cc19e5 100644 --- a/src/include/catalog/index.h +++ b/src/include/catalog/index.h @@ -100,6 +100,9 @@ extern Oid index_concurrently_create_copy(Relation heapRelation, Oid tablespaceOid, const char *newName); +extern NullableDatum *get_index_stattargets(Oid indexid, + IndexInfo *indInfo); + extern void index_concurrently_build(Oid heapRelationId, Oid indexRelationId); diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h index c2976905e4d..569cc2184b3 100644 --- a/src/include/commands/cluster.h +++ b/src/include/commands/cluster.h @@ -13,10 +13,15 @@ #ifndef CLUSTER_H #define CLUSTER_H +#include "nodes/execnodes.h" #include "nodes/parsenodes.h" #include "parser/parse_node.h" +#include "replication/logical.h" #include "storage/lock.h" +#include "storage/relfilelocator.h" #include "utils/relcache.h" +#include "utils/resowner.h" +#include "utils/tuplestore.h" /* flag bits for ClusterParams->options */ @@ -24,6 +29,7 @@ #define CLUOPT_RECHECK 0x02 /* recheck relation state */ #define CLUOPT_RECHECK_ISCLUSTERED 0x04 /* recheck relation state for * indisclustered */ +#define CLUOPT_CONCURRENT 0x08 /* allow concurrent data changes */ /* options for CLUSTER */ typedef struct ClusterParams @@ -46,14 +52,90 @@ typedef enum ClusterCommand CLUSTER_COMMAND_VACUUM } ClusterCommand; +/* + * The following definitions are used by REPACK CONCURRENTLY. + */ + +extern RelFileLocator repacked_rel_locator; +extern RelFileLocator repacked_rel_toast_locator; + +typedef enum +{ + CHANGE_INSERT, + CHANGE_UPDATE_OLD, + CHANGE_UPDATE_NEW, + CHANGE_DELETE, + CHANGE_TRUNCATE +} ConcurrentChangeKind; + +typedef struct ConcurrentChange +{ + /* See the enum above. */ + ConcurrentChangeKind kind; + + /* + * The actual tuple. + * + * The tuple data follows the ConcurrentChange structure. Before use make + * sure the tuple is correctly aligned (ConcurrentChange can be stored as + * bytea) and that tuple->t_data is fixed. + */ + HeapTupleData tup_data; +} ConcurrentChange; + +#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \ + sizeof(HeapTupleData)) + +/* + * Logical decoding state. + * + * Here we store the data changes that we decode from WAL while the table + * contents is being copied to a new storage. Also the necessary metadata + * needed to apply these changes to the table is stored here. + */ +typedef struct RepackDecodingState +{ + /* The relation whose changes we're decoding. */ + Oid relid; + + /* + * Decoded changes are stored here. Although we try to avoid excessive + * batches, it can happen that the changes need to be stored to disk. The + * tuplestore does this transparently. + */ + Tuplestorestate *tstore; + + /* The current number of changes in tstore. */ + double nchanges; + + /* + * Descriptor to store the ConcurrentChange structure serialized (bytea). + * We can't store the tuple directly because tuplestore only supports + * minimum tuple and we may need to transfer OID system column from the + * output plugin. Also we need to transfer the change kind, so it's better + * to put everything in the structure than to use 2 tuplestores "in + * parallel". + */ + TupleDesc tupdesc_change; + + /* Tuple descriptor needed to update indexes. */ + TupleDesc tupdesc; + + /* Slot to retrieve data from tstore. */ + TupleTableSlot *tsslot; + + ResourceOwner resowner; +} RepackDecodingState; + extern void cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel); extern void cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, - ClusterCommand cmd); + ClusterCommand cmd, bool isTopLevel); extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode, ClusterCommand cmd); extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal); - +extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx, + XLogRecPtr end_of_wal); extern Oid make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod, char relpersistence, LOCKMODE lockmode); extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, @@ -61,6 +143,7 @@ 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); diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h index 7644267e14f..6b1b1a4c1a7 100644 --- a/src/include/commands/progress.h +++ b/src/include/commands/progress.h @@ -67,10 +67,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). @@ -83,9 +85,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 8 +#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP 8 /* Commands of PROGRESS_REPACK */ #define PROGRESS_REPACK_COMMAND_REPACK 1 diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4ef76c852f5..de091ceb04a 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3931,6 +3931,7 @@ typedef struct RepackStmt RangeVar *relation; /* relation being repacked */ char *indexname; /* order tuples by this index */ List *params; /* list of DefElem nodes */ + bool concurrent; /* allow concurrent access? */ } RepackStmt; diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h index 6d4d2d1814c..802fc4b0823 100644 --- a/src/include/replication/snapbuild.h +++ b/src/include/replication/snapbuild.h @@ -73,6 +73,7 @@ extern void FreeSnapshotBuilder(SnapBuild *builder); extern void SnapBuildSnapDecRefcount(Snapshot snap); extern Snapshot SnapBuildInitialSnapshot(SnapBuild *builder); +extern Snapshot SnapBuildInitialSnapshotForRepack(SnapBuild *builder); extern Snapshot SnapBuildMVCCFromHistoric(Snapshot snapshot, bool in_place); extern const char *SnapBuildExportSnapshot(SnapBuild *builder); extern void SnapBuildClearExportedSnapshot(void); diff --git a/src/include/storage/lockdefs.h b/src/include/storage/lockdefs.h index 7f3ba0352f6..2739327b0da 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/lwlocklist.h b/src/include/storage/lwlocklist.h index 932024b1b0b..fe9d85e5f95 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -84,3 +84,4 @@ PG_LWLOCK(50, DSMRegistry) PG_LWLOCK(51, InjectionPoint) PG_LWLOCK(52, SerialControl) PG_LWLOCK(53, AioWorkerSubmissionQueue) +PG_LWLOCK(54, RepackedRels) diff --git a/src/include/utils/snapmgr.h b/src/include/utils/snapmgr.h index 147b190210a..5eeabdc6c4f 100644 --- a/src/include/utils/snapmgr.h +++ b/src/include/utils/snapmgr.h @@ -61,6 +61,8 @@ extern Snapshot GetLatestSnapshot(void); extern void SnapshotSetCommandId(CommandId curcid); extern Snapshot CopySnapshot(Snapshot snapshot); +extern void FreeSnapshot(Snapshot snapshot); + extern Snapshot GetCatalogSnapshot(Oid relid); extern Snapshot GetNonHistoricCatalogSnapshot(Oid relid); extern void InvalidateCatalogSnapshot(void); diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 84ca2dc3778..086c61f4ef4 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1969,17 +1969,17 @@ pg_stat_progress_cluster| 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 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 cluster_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.param8 AS heap_blks_total, + s.param9 AS heap_blks_scanned, + s.param10 AS index_rebuild_count FROM (pg_stat_get_progress_info('CLUSTER'::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_copy| SELECT s.pid, @@ -2055,17 +2055,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 c7ea8fb93ca..e89db0a2ee7 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -477,6 +477,8 @@ CompressFileHandle CompressionLocation CompressorState ComputeXidHorizonsResult +ConcurrentChange +ConcurrentChangeKind ConditionVariable ConditionVariableMinimallyPadded ConditionalStack @@ -1239,6 +1241,7 @@ IndexElem IndexFetchHeapData IndexFetchTableData IndexInfo +IndexInsertState IndexList IndexOnlyScan IndexOnlyScanState @@ -2507,6 +2510,7 @@ ReorderBufferTupleCidKey ReorderBufferUpdateProgressTxnCB ReorderTuple RepOriginId +RepackDecodingState RepackStmt ReparameterizeForeignPathByChild_function ReplaceVarsFromTargetList_context -- 2.43.5 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v12-0005-Preserve-visibility-information-of-the-concurrent-da.patch From 16133cdbdcfc338d8a30ae058bdf7930a72ae888 Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Tue, 1 Apr 2025 13:48:57 +0200 Subject: [PATCH 5/9] Preserve visibility information of the concurrent data changes. As explained in the commit message of the preceding patch of the series, the data changes done by applications while REPACK CONCURRENTLY is copying the table contents to a new file are decoded from WAL and eventually also applied to the new file. To reduce the complexity a little bit, the preceding patch uses the current transaction (i.e. transaction opened by the REPACK command) to execute those INSERT, UPDATE and DELETE commands. However, REPACK is not expected to change visibility of tuples. Therefore, this patch fixes the handling of the "concurrent data changes". It ensures that tuples written into the new table have the same XID and command ID (CID) as they had in the old table. To "replay" an UPDATE or DELETE command on the new table, we need the appropriate snapshot to find the previous tuple version in the new table. The (historic) snapshot we used to decode the UPDATE / DELETE should (by definition) see the state of the catalog prior to that UPDATE / DELETE. Thus we can use the same snapshot to find the "old tuple" for UPDATE / DELETE in the new table if: 1) REPACK CONCURRENTLY preserves visibility information of all tuples - that's the purpose of this part of the patch series. 2) The table being REPACKed is treated as a system catalog by all transactions that modify its data. This ensures that reorderbuffer.c generates a new snapshot for each data change in the table. We ensure 2) by maintaining a shared hashtable of tables being REPACKed CONCURRENTLY and by adjusting the RelationIsAccessibleInLogicalDecoding() macro so it checks this hashtable. (The corresponding flag is also added to the relation cache, so that the shared hashtable does not have to be accessed too often.) It's essential that after adding an entry to the hashtable we wait for completion of all the transactions that might have started to modify our table before our entry has was added. We achieve that by upgrading our lock on the table to ShareLock temporarily: as soon as we acquire it, no DML command should be running on the table. (This lock upgrade shouldn't cause any deadlock because we care to not hold a lock on other objects at the same time.) As long as we preserve the tuple visibility information (which includes XID), it's important to avoid logical decoding of the WAL generated by DMLs on the new table: the logical decoding subsystem probably does not expect that the incoming WAL records contain XIDs of an already decoded transactions. (And of course, repeated decoding would be wasted effort.) --- src/backend/access/common/toast_internals.c | 3 +- src/backend/access/heap/heapam.c | 82 ++-- src/backend/access/heap/heapam_handler.c | 14 +- src/backend/access/transam/xact.c | 52 +++ src/backend/commands/cluster.c | 406 +++++++++++++++++- src/backend/replication/logical/decode.c | 77 +++- src/backend/replication/logical/snapbuild.c | 22 +- .../pgoutput_repack/pgoutput_repack.c | 68 ++- src/backend/storage/ipc/ipci.c | 2 + src/backend/utils/cache/inval.c | 21 + src/backend/utils/cache/relcache.c | 4 + src/include/access/heapam.h | 15 +- src/include/access/heapam_xlog.h | 2 + src/include/access/xact.h | 2 + src/include/commands/cluster.h | 22 + src/include/utils/inval.h | 2 + src/include/utils/rel.h | 7 +- src/include/utils/snapshot.h | 3 + src/tools/pgindent/typedefs.list | 1 + 19 files changed, 722 insertions(+), 83 deletions(-) diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c index 7d8be8346ce..75d889ec72c 100644 --- a/src/backend/access/common/toast_internals.c +++ b/src/backend/access/common/toast_internals.c @@ -320,7 +320,8 @@ toast_save_datum(Relation rel, Datum value, memcpy(VARDATA(&chunk_data), data_p, chunk_size); toasttup = heap_form_tuple(toasttupDesc, t_values, t_isnull); - heap_insert(toastrel, toasttup, mycid, options, NULL); + heap_insert(toastrel, toasttup, GetCurrentTransactionId(), mycid, + options, NULL); /* * Create the index entry. We cheat a little here by not using diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index cedaa195cb6..8299f3b3ded 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -60,7 +60,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 wal_logical); #ifdef USE_ASSERT_CHECKING static void check_lock_if_inplace_updateable_rel(Relation relation, ItemPointer otid, @@ -2087,7 +2088,7 @@ ReleaseBulkInsertStatePin(BulkInsertState bistate) /* * heap_insert - insert tuple into a heap * - * The new tuple is stamped with current transaction ID and the specified + * The new tuple is stamped with specified transaction ID and the specified * command ID. * * See table_tuple_insert for comments about most of the input flags, except @@ -2103,15 +2104,16 @@ ReleaseBulkInsertStatePin(BulkInsertState bistate) * reflected into *tup. */ void -heap_insert(Relation relation, HeapTuple tup, CommandId cid, - int options, BulkInsertState bistate) +heap_insert(Relation relation, HeapTuple tup, TransactionId xid, + CommandId cid, int options, BulkInsertState bistate) { - TransactionId xid = GetCurrentTransactionId(); HeapTuple heaptup; Buffer buffer; Buffer vmbuffer = InvalidBuffer; bool all_visible_cleared = false; + Assert(TransactionIdIsValid(xid)); + /* Cheap, simplistic check that the tuple matches the rel's rowtype. */ Assert(HeapTupleHeaderGetNatts(tup->t_data) <= RelationGetNumberOfAttributes(relation)); @@ -2191,8 +2193,15 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, /* * If this is a catalog, we need to transmit combo CIDs to properly * decode, so log that as well. + * + * HEAP_INSERT_NO_LOGICAL should be set when applying data changes + * done by other transactions during REPACK CONCURRENTLY. In such a + * case, the insertion should not be decoded at all - see + * heap_decode(). (It's also set by raw_heap_insert() for TOAST, but + * TOAST does not pass this test anyway.) */ - if (RelationIsAccessibleInLogicalDecoding(relation)) + if ((options & HEAP_INSERT_NO_LOGICAL) == 0 && + RelationIsAccessibleInLogicalDecoding(relation)) log_heap_new_cid(relation, heaptup); /* @@ -2736,7 +2745,8 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, void simple_heap_insert(Relation relation, HeapTuple tup) { - heap_insert(relation, tup, GetCurrentCommandId(true), 0, NULL); + heap_insert(relation, tup, GetCurrentTransactionId(), + GetCurrentCommandId(true), 0, NULL); } /* @@ -2793,11 +2803,11 @@ xmax_infomask_changed(uint16 new_infomask, uint16 old_infomask) */ TM_Result heap_delete(Relation relation, ItemPointer tid, - CommandId cid, Snapshot crosscheck, bool wait, - TM_FailureData *tmfd, bool changingPart) + TransactionId xid, CommandId cid, Snapshot crosscheck, bool wait, + TM_FailureData *tmfd, bool changingPart, + bool wal_logical) { TM_Result result; - TransactionId xid = GetCurrentTransactionId(); ItemId lp; HeapTupleData tp; Page page; @@ -2814,6 +2824,7 @@ heap_delete(Relation relation, ItemPointer tid, bool old_key_copied = false; Assert(ItemPointerIsValid(tid)); + Assert(TransactionIdIsValid(xid)); /* * Forbid this during a parallel operation, lest it allocate a combo CID. @@ -3039,7 +3050,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 = wal_logical ? + ExtractReplicaIdentity(relation, &tp, true, &old_key_copied) : NULL; /* * If this is the first possibly-multixact-able operation in the current @@ -3107,8 +3119,12 @@ l1: /* * For logical decode we need combo CIDs to properly decode the * catalog + * + * Like in heap_insert(), visibility is unchanged when called from + * VACUUM FULL / CLUSTER. */ - if (RelationIsAccessibleInLogicalDecoding(relation)) + if (wal_logical && + RelationIsAccessibleInLogicalDecoding(relation)) log_heap_new_cid(relation, &tp); xlrec.flags = 0; @@ -3129,6 +3145,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 (!wal_logical) + xlrec.flags |= XLH_DELETE_NO_LOGICAL; + XLogBeginInsert(); XLogRegisterData(&xlrec, SizeOfHeapDelete); @@ -3218,10 +3243,11 @@ simple_heap_delete(Relation relation, ItemPointer tid) TM_Result result; TM_FailureData tmfd; - result = heap_delete(relation, tid, + result = heap_delete(relation, tid, GetCurrentTransactionId(), GetCurrentCommandId(true), InvalidSnapshot, true /* wait for commit */ , - &tmfd, false /* changingPart */ ); + &tmfd, false, /* changingPart */ + true /* wal_logical */ ); switch (result) { case TM_SelfModified: @@ -3260,12 +3286,11 @@ simple_heap_delete(Relation relation, ItemPointer tid) */ TM_Result heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, - CommandId cid, Snapshot crosscheck, bool wait, - TM_FailureData *tmfd, LockTupleMode *lockmode, - TU_UpdateIndexes *update_indexes) + TransactionId xid, CommandId cid, Snapshot crosscheck, + bool wait, TM_FailureData *tmfd, LockTupleMode *lockmode, + TU_UpdateIndexes *update_indexes, bool wal_logical) { TM_Result result; - TransactionId xid = GetCurrentTransactionId(); Bitmapset *hot_attrs; Bitmapset *sum_attrs; Bitmapset *key_attrs; @@ -3305,6 +3330,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, infomask2_new_tuple; Assert(ItemPointerIsValid(otid)); + Assert(TransactionIdIsValid(xid)); /* Cheap, simplistic check that the tuple matches the rel's rowtype. */ Assert(HeapTupleHeaderGetNatts(newtup->t_data) <= @@ -4142,8 +4168,12 @@ l2: /* * For logical decoding we need combo CIDs to properly decode the * catalog. + * + * Like in heap_insert(), visibility is unchanged when called from + * VACUUM FULL / CLUSTER. */ - if (RelationIsAccessibleInLogicalDecoding(relation)) + if (wal_logical && + RelationIsAccessibleInLogicalDecoding(relation)) { log_heap_new_cid(relation, &oldtup); log_heap_new_cid(relation, heaptup); @@ -4153,7 +4183,8 @@ l2: newbuf, &oldtup, heaptup, old_key_tuple, all_visible_cleared, - all_visible_cleared_new); + all_visible_cleared_new, + wal_logical); if (newbuf != buffer) { PageSetLSN(BufferGetPage(newbuf), recptr); @@ -4508,10 +4539,10 @@ simple_heap_update(Relation relation, ItemPointer otid, HeapTuple tup, TM_FailureData tmfd; LockTupleMode lockmode; - result = heap_update(relation, otid, tup, + result = heap_update(relation, otid, tup, GetCurrentTransactionId(), GetCurrentCommandId(true), InvalidSnapshot, true /* wait for commit */ , - &tmfd, &lockmode, update_indexes); + &tmfd, &lockmode, update_indexes, true); switch (result) { case TM_SelfModified: @@ -8844,7 +8875,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 wal_logical) { xl_heap_update xlrec; xl_heap_header xlhdr; @@ -8855,10 +8887,12 @@ 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; bool init; int bufflags; + need_tuple_data = RelationIsLogicallyLogged(reln) && wal_logical; + /* Caller should not call me on a non-WAL-logged relation */ Assert(RelationNeedsWAL(reln)); diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 371afa6ad59..ea1d6f299b3 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -256,7 +256,8 @@ heapam_tuple_insert(Relation relation, TupleTableSlot *slot, CommandId cid, tuple->t_tableOid = slot->tts_tableOid; /* Perform the insertion, and copy the resulting ItemPointer */ - heap_insert(relation, tuple, cid, options, bistate); + heap_insert(relation, tuple, GetCurrentTransactionId(), cid, options, + bistate); ItemPointerCopy(&tuple->t_self, &slot->tts_tid); if (shouldFree) @@ -279,7 +280,8 @@ heapam_tuple_insert_speculative(Relation relation, TupleTableSlot *slot, options |= HEAP_INSERT_SPECULATIVE; /* Perform the insertion, and copy the resulting ItemPointer */ - heap_insert(relation, tuple, cid, options, bistate); + heap_insert(relation, tuple, GetCurrentTransactionId(), cid, options, + bistate); ItemPointerCopy(&tuple->t_self, &slot->tts_tid); if (shouldFree) @@ -313,7 +315,8 @@ heapam_tuple_delete(Relation relation, ItemPointer tid, CommandId cid, * the storage itself is cleaning the dead tuples by itself, it is the * time to call the index tuple deletion also. */ - return heap_delete(relation, tid, cid, crosscheck, wait, tmfd, changingPart); + return heap_delete(relation, tid, GetCurrentTransactionId(), cid, + crosscheck, wait, tmfd, changingPart, true); } @@ -331,8 +334,9 @@ heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot, slot->tts_tableOid = RelationGetRelid(relation); tuple->t_tableOid = slot->tts_tableOid; - result = heap_update(relation, otid, tuple, cid, crosscheck, wait, - tmfd, lockmode, update_indexes); + result = heap_update(relation, otid, tuple, GetCurrentTransactionId(), + cid, crosscheck, wait, + tmfd, lockmode, update_indexes, true); ItemPointerCopy(&tuple->t_self, &slot->tts_tid); /* diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 23f2de587a1..3db4cac030e 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -126,6 +126,18 @@ static FullTransactionId XactTopFullTransactionId = {InvalidTransactionId}; static int nParallelCurrentXids = 0; static TransactionId *ParallelCurrentXids; +/* + * Another case that requires TransactionIdIsCurrentTransactionId() to behave + * specially is when REPACK CONCURRENTLY is processing data changes made in + * the old storage of a table by other transactions. When applying the changes + * to the new storage, the backend executing the CLUSTER command needs to act + * on behalf on those other transactions. The transactions responsible for the + * changes in the old storage are stored in this array, sorted by + * xidComparator. + */ +static int nRepackCurrentXids = 0; +static TransactionId *RepackCurrentXids = NULL; + /* * Miscellaneous flag bits to record events which occur on the top level * transaction. These flags are only persisted in MyXactFlags and are intended @@ -973,6 +985,8 @@ TransactionIdIsCurrentTransactionId(TransactionId xid) int low, high; + Assert(nRepackCurrentXids == 0); + low = 0; high = nParallelCurrentXids - 1; while (low <= high) @@ -992,6 +1006,21 @@ TransactionIdIsCurrentTransactionId(TransactionId xid) return false; } + /* + * When executing CLUSTER CONCURRENTLY, the array of current transactions + * is given. + */ + if (nRepackCurrentXids > 0) + { + Assert(nParallelCurrentXids == 0); + + return bsearch(&xid, + RepackCurrentXids, + nRepackCurrentXids, + sizeof(TransactionId), + xidComparator) != NULL; + } + /* * We will return true for the Xid of the current subtransaction, any of * its subcommitted children, any of its parents, or any of their @@ -5649,6 +5678,29 @@ EndParallelWorkerTransaction(void) CurrentTransactionState->blockState = TBLOCK_DEFAULT; } +/* + * SetRepackCurrentXids + * Set the XID array that TransactionIdIsCurrentTransactionId() should + * use. + */ +void +SetRepackCurrentXids(TransactionId *xip, int xcnt) +{ + RepackCurrentXids = xip; + nRepackCurrentXids = xcnt; +} + +/* + * ResetRepackCurrentXids + * Undo the effect of SetRepackCurrentXids(). + */ +void +ResetRepackCurrentXids(void) +{ + RepackCurrentXids = NULL; + nRepackCurrentXids = 0; +} + /* * ShowTransactionState * Debug support diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index 4d08a28ff7e..e30c1a78e9c 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -90,6 +90,11 @@ typedef struct * The following definitions are used for concurrent processing. */ +/* + * OID of the table being repacked by this backend. + */ +static Oid repacked_rel = InvalidOid; + /* * The locators are used to avoid logical decoding of data that we do not need * for our table. @@ -133,8 +138,10 @@ static List *get_tables_to_cluster_partitioned(MemoryContext cluster_context, ClusterCommand cmd); static bool cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd); -static void begin_concurrent_repack(Relation rel); -static void end_concurrent_repack(void); +static void begin_concurrent_repack(Relation rel, Relation *index_p, + bool *entered_p); +static void end_concurrent_repack(bool error); +static void cluster_before_shmem_exit_callback(int code, Datum arg); static LogicalDecodingContext *setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc); @@ -154,6 +161,7 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target, ConcurrentChange *change); static HeapTuple find_target_tuple(Relation rel, ScanKey key, int nkeys, HeapTuple tup_key, + Snapshot snapshot, IndexInsertState *iistate, TupleTableSlot *ident_slot, IndexScanDesc *scan_p); @@ -379,6 +387,8 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, const char *cmd_str = CLUSTER_COMMAND_STR(cmd); bool concurrent = ((params->options & CLUOPT_CONCURRENT) != 0); LOCKMODE lmode; + bool entered, + success; /* * Check that the correct lock is held. The lock mode is @@ -536,23 +546,30 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params, TransferPredicateLocksToHeapRelation(OldHeap); /* rebuild_relation does all the dirty work */ + entered = false; + success = false; PG_TRY(); { /* - * For concurrent processing, make sure that our logical decoding - * ignores data changes of other tables than the one we are - * processing. + * For concurrent processing, make sure that + * + * 1) our logical decoding ignores data changes of other tables than + * the one we are processing. + * + * 2) other transactions treat this table as if it was a system / user + * catalog, and WAL the relevant additional information. */ if (concurrent) - begin_concurrent_repack(OldHeap); + begin_concurrent_repack(OldHeap, &index, &entered); rebuild_relation(OldHeap, index, verbose, cmd, concurrent, save_userid); + success = true; } PG_FINALLY(); { - if (concurrent) - end_concurrent_repack(); + if (concurrent && entered) + end_concurrent_repack(!success); } PG_END_TRY(); @@ -2162,6 +2179,47 @@ cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd) #define REPL_PLUGIN_NAME "pgoutput_repack" +/* + * Each relation being processed by REPACK CONCURRENTLY must be in the + * repackedRels hashtable. + */ +typedef struct RepackedRel +{ + Oid relid; + Oid dbid; +} RepackedRel; + +static HTAB *RepackedRelsHash = NULL; + +/* + * Maximum number of entries in the hashtable. + * + * A replication slot is needed for the processing, so use this GUC to + * allocate memory for the hashtable. + */ +#define MAX_REPACKED_RELS (max_replication_slots) + +Size +RepackShmemSize(void) +{ + return hash_estimate_size(MAX_REPACKED_RELS, sizeof(RepackedRel)); +} + +void +RepackShmemInit(void) +{ + HASHCTL info; + + info.keysize = sizeof(RepackedRel); + info.entrysize = info.keysize; + + RepackedRelsHash = ShmemInitHash("Repacked Relations", + MAX_REPACKED_RELS, + MAX_REPACKED_RELS, + &info, + HASH_ELEM | HASH_BLOBS); +} + /* * Call this function before REPACK CONCURRENTLY starts to setup logical * decoding. It makes sure that other users of the table put enough @@ -2176,11 +2234,120 @@ cluster_is_permitted_for_relation(Oid relid, Oid userid, ClusterCommand cmd) * * Note that TOAST table needs no attention here as it's not scanned using * historic snapshot. + * + * 'index_p' is in/out argument because the function unlocks the index + * temporarily. + * + * 'enter_p' receives a bool value telling whether relation OID was entered + * into RepackedRelsHash or not. */ static void -begin_concurrent_repack(Relation rel) +begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p) { - Oid toastrelid; + Oid relid, + toastrelid; + Relation index = NULL; + Oid indexid = InvalidOid; + RepackedRel key, + *entry; + bool found; + static bool before_shmem_exit_callback_setup = false; + + relid = RelationGetRelid(rel); + index = index_p ? *index_p : NULL; + + /* + * Make sure that we do not leave an entry in RepackedRelsHash if exiting + * due to FATAL. + */ + if (!before_shmem_exit_callback_setup) + { + before_shmem_exit(cluster_before_shmem_exit_callback, 0); + before_shmem_exit_callback_setup = true; + } + + memset(&key, 0, sizeof(key)); + key.relid = relid; + key.dbid = MyDatabaseId; + + *entered_p = false; + LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE); + entry = (RepackedRel *) + hash_search(RepackedRelsHash, &key, HASH_ENTER_NULL, &found); + if (found) + { + /* + * Since REPACK CONCURRENTLY takes ShareRowExclusiveLock, a conflict + * should occur much earlier. However that lock may be released + * temporarily, see below. Anyway, we should complain whatever the + * reason of the conflict might be. + */ + ereport(ERROR, + (errmsg("relation \"%s\" is already being processed by REPACK CONCURRENTLY", + RelationGetRelationName(rel)))); + } + if (entry == NULL) + ereport(ERROR, + (errmsg("too many requests for REPACK CONCURRENTLY at a time")), + (errhint("Please consider increasing the \"max_replication_slots\" configuration parameter."))); + + /* + * Even if anything fails below, the caller has to do cleanup in the + * shared memory. + */ + *entered_p = true; + + /* + * Enable the callback to remove the entry in case of exit. We should not + * do this earlier, otherwise an attempt to insert already existing entry + * could make us remove that entry (inserted by another backend) during + * ERROR handling. + */ + Assert(!OidIsValid(repacked_rel)); + repacked_rel = relid; + + LWLockRelease(RepackedRelsLock); + + /* + * Make sure that other backends are aware of the new hash entry as soon + * as they open our table. + */ + CacheInvalidateRelcacheImmediate(relid); + + /* + * Also make sure that the existing users of the table update their + * relcache entry as soon as they try to run DML commands on it. + * + * ShareLock is the weakest lock that conflicts with DMLs. If any backend + * has a lower lock, we assume it'll accept our invalidation message when + * it changes the lock mode. + * + * Before upgrading the lock on the relation, close the index temporarily + * to avoid a deadlock if another backend running DML already has its lock + * (ShareLock) on the table and waits for the lock on the index. + */ + if (index) + { + indexid = RelationGetRelid(index); + index_close(index, ShareUpdateExclusiveLock); + } + LockRelationOid(relid, ShareLock); + UnlockRelationOid(relid, ShareLock); + if (OidIsValid(indexid)) + { + /* + * Re-open the index and check that it hasn't changed while unlocked. + */ + check_index_is_clusterable(rel, indexid, ShareUpdateExclusiveLock, + CLUSTER_COMMAND_REPACK); + + /* + * Return the new relcache entry to the caller. (It's been locked by + * the call above.) + */ + index = index_open(indexid, NoLock); + *index_p = index; + } /* Avoid logical decoding of other relations by this backend. */ repacked_rel_locator = rel->rd_locator; @@ -2198,15 +2365,122 @@ begin_concurrent_repack(Relation rel) /* * Call this when done with REPACK CONCURRENTLY. + * + * 'error' tells whether the function is being called in order to handle + * error. */ static void -end_concurrent_repack(void) +end_concurrent_repack(bool error) { + RepackedRel key; + RepackedRel *entry = NULL; + Oid relid = repacked_rel; + + /* Remove the relation from the hash if we managed to insert one. */ + if (OidIsValid(repacked_rel)) + { + memset(&key, 0, sizeof(key)); + key.relid = repacked_rel; + key.dbid = MyDatabaseId; + LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE); + entry = hash_search(RepackedRelsHash, &key, HASH_REMOVE, NULL); + LWLockRelease(RepackedRelsLock); + + /* + * Make others refresh their information whether they should still + * treat the table as catalog from the perspective of writing WAL. + * + * XXX Unlike entering the entry into the hashtable, we do not bother + * with locking and unlocking the table here: + * + * 1) On normal completion (and sometimes even on ERROR), the caller + * is already holding AccessExclusiveLock on the table, so there + * should be no relcache reference unaware of this change. + * + * 2) In the other cases, the worst scenario is that the other + * backends will write unnecessary information to WAL until they close + * the relation. + * + * Should we use ShareLock mode to fix 2) at least for the non-FATAL + * errors? (Our before_shmem_exit callback is in charge of FATAL, and + * that probably should not try to acquire any lock.) + */ + CacheInvalidateRelcacheImmediate(repacked_rel); + + /* + * By clearing this variable we also disable + * cluster_before_shmem_exit_callback(). + */ + repacked_rel = InvalidOid; + } + /* * Restore normal function of (future) logical decoding for this backend. */ repacked_rel_locator.relNumber = InvalidOid; repacked_rel_toast_locator.relNumber = InvalidOid; + + /* + * On normal completion (!error), we should not really fail to remove the + * entry. But if it wasn't there for any reason, raise ERROR to make sure + * the transaction is aborted: if other transactions, while changing the + * contents of the relation, didn't know that REPACK CONCURRENTLY was in + * progress, they could have missed to WAL enough information, and thus we + * could have produced an inconsistent table contents. + * + * On the other hand, if we are already handling an error, there's no + * reason to worry about inconsistent contents of the new storage because + * the transaction is going to be rolled back anyway. Furthermore, by + * raising ERROR here we'd shadow the original error. + */ + if (!error) + { + char *relname; + + if (OidIsValid(relid) && entry == NULL) + { + relname = get_rel_name(relid); + if (!relname) + ereport(ERROR, + (errmsg("cache lookup failed for relation %u", + relid))); + + ereport(ERROR, + (errmsg("relation \"%s\" not found among repacked relations", + relname))); + } + } +} + +/* + * A wrapper to call end_concurrent_repack() as a before_shmem_exit callback. + */ +static void +cluster_before_shmem_exit_callback(int code, Datum arg) +{ + if (OidIsValid(repacked_rel)) + end_concurrent_repack(true); +} + +/* + * Check if relation is currently being processed by REPACK CONCURRENTLY. + */ +bool +is_concurrent_repack_in_progress(Oid relid) +{ + RepackedRel key, + *entry; + + memset(&key, 0, sizeof(key)); + key.relid = relid; + key.dbid = MyDatabaseId; + + LWLockAcquire(RepackedRelsLock, LW_SHARED); + entry = (RepackedRel *) + hash_search(RepackedRelsHash, &key, HASH_FIND, NULL); + LWLockRelease(RepackedRelsLock); + + return entry != NULL; } /* @@ -2268,6 +2542,9 @@ setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc) dstate->relid = relid; dstate->tstore = tuplestore_begin_heap(false, false, maintenance_work_mem); +#ifdef USE_ASSERT_CHECKING + dstate->last_change_xid = InvalidTransactionId; +#endif dstate->tupdesc = tupdesc; @@ -2415,6 +2692,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, char *change_raw, *src; ConcurrentChange change; + Snapshot snapshot; bool isnull[1]; Datum values[1]; @@ -2483,8 +2761,30 @@ apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, /* * Find the tuple to be updated or deleted. + * + * As the table being REPACKed concurrently is treated like a + * catalog, new CID is WAL-logged and decoded. And since we use + * the same XID that the original DMLs did, the snapshot used for + * the logical decoding (by now converted to a non-historic MVCC + * snapshot) should see the tuples inserted previously into the + * new heap and/or updated there. + */ + snapshot = change.snapshot; + + /* + * Set what should be considered current transaction (and + * subtransactions) during visibility check. + * + * Note that this snapshot was created from a historic snapshot + * using SnapBuildMVCCFromHistoric(), which does not touch + * 'subxip'. Thus, unlike in a regular MVCC snapshot, the array + * only contains the transactions whose data changes we are + * applying, and its subtransactions. That's exactly what we need + * to check if particular xact is a "current transaction:". */ - tup_exist = find_target_tuple(rel, key, nkeys, tup_key, + SetRepackCurrentXids(snapshot->subxip, snapshot->subxcnt); + + tup_exist = find_target_tuple(rel, key, nkeys, tup_key, snapshot, iistate, ident_slot, &ind_scan); if (tup_exist == NULL) elog(ERROR, "Failed to find target tuple"); @@ -2495,6 +2795,8 @@ apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, else apply_concurrent_delete(rel, tup_exist, &change); + ResetRepackCurrentXids(); + if (tup_old != NULL) { pfree(tup_old); @@ -2507,11 +2809,14 @@ apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, else elog(ERROR, "Unrecognized kind of change: %d", change.kind); - /* If there's any change, make it visible to the next iteration. */ - if (change.kind != CHANGE_UPDATE_OLD) + /* Free the snapshot if this is the last change that needed it. */ + Assert(change.snapshot->active_count > 0); + change.snapshot->active_count--; + if (change.snapshot->active_count == 0) { - CommandCounterIncrement(); - UpdateActiveSnapshotCommandId(); + if (change.snapshot == dstate->snapshot) + dstate->snapshot = NULL; + FreeSnapshot(change.snapshot); } /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */ @@ -2531,10 +2836,30 @@ static void apply_concurrent_insert(Relation rel, ConcurrentChange *change, HeapTuple tup, IndexInsertState *iistate, TupleTableSlot *index_slot) { + Snapshot snapshot = change->snapshot; List *recheck; + /* + * For INSERT, the visibility information is not important, but we use the + * snapshot to get CID. Index functions might need the whole snapshot + * anyway. + */ + SetRepackCurrentXids(snapshot->subxip, snapshot->subxcnt); + + /* + * Write the tuple into the new heap. + * + * The snapshot is the one we used to decode the insert (though converted + * to "non-historic" MVCC snapshot), i.e. the snapshot's curcid is the + * tuple CID incremented by one (due to the "new CID" WAL record that got + * written along with the INSERT record). Thus if we want to use the + * original CID, we need to subtract 1 from curcid. + */ + Assert(snapshot->curcid != InvalidCommandId && + snapshot->curcid > FirstCommandId); - simple_heap_insert(rel, tup); + heap_insert(rel, tup, change->xid, snapshot->curcid - 1, + HEAP_INSERT_NO_LOGICAL, NULL); /* * Update indexes. @@ -2542,6 +2867,7 @@ apply_concurrent_insert(Relation rel, ConcurrentChange *change, HeapTuple tup, * In case functions in the index need the active snapshot and caller * hasn't set one. */ + PushActiveSnapshot(snapshot); ExecStoreHeapTuple(tup, index_slot, false); recheck = ExecInsertIndexTuples(iistate->rri, index_slot, @@ -2552,6 +2878,8 @@ apply_concurrent_insert(Relation rel, ConcurrentChange *change, HeapTuple tup, NIL, /* arbiterIndexes */ false /* onlySummarizing */ ); + PopActiveSnapshot(); + ResetRepackCurrentXids(); /* * If recheck is required, it must have been preformed on the source @@ -2569,18 +2897,36 @@ apply_concurrent_update(Relation rel, HeapTuple tup, HeapTuple tup_target, TupleTableSlot *index_slot) { List *recheck; + LockTupleMode lockmode; TU_UpdateIndexes update_indexes; + TM_Result res; + Snapshot snapshot = change->snapshot; + TM_FailureData tmfd; /* * Write the new tuple into the new heap. ('tup' gets the TID assigned * here.) + * + * Regarding CID, see the comment in apply_concurrent_insert(). */ - simple_heap_update(rel, &tup_target->t_self, tup, &update_indexes); + Assert(snapshot->curcid != InvalidCommandId && + snapshot->curcid > FirstCommandId); + + res = heap_update(rel, &tup_target->t_self, tup, + change->xid, snapshot->curcid - 1, + InvalidSnapshot, + false, /* no wait - only we are doing changes */ + &tmfd, &lockmode, &update_indexes, + /* wal_logical */ + false); + if (res != TM_Ok) + ereport(ERROR, (errmsg("failed to apply concurrent UPDATE"))); ExecStoreHeapTuple(tup, index_slot, false); if (update_indexes != TU_None) { + PushActiveSnapshot(snapshot); recheck = ExecInsertIndexTuples(iistate->rri, index_slot, iistate->estate, @@ -2590,6 +2936,7 @@ apply_concurrent_update(Relation rel, HeapTuple tup, HeapTuple tup_target, NIL, /* arbiterIndexes */ /* onlySummarizing */ update_indexes == TU_Summarizing); + PopActiveSnapshot(); list_free(recheck); } @@ -2600,7 +2947,22 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target, ConcurrentChange *change) { - simple_heap_delete(rel, &tup_target->t_self); + TM_Result res; + TM_FailureData tmfd; + Snapshot snapshot = change->snapshot; + + /* Regarding CID, see the comment in apply_concurrent_insert(). */ + Assert(snapshot->curcid != InvalidCommandId && + snapshot->curcid > FirstCommandId); + + res = heap_delete(rel, &tup_target->t_self, change->xid, + snapshot->curcid - 1, InvalidSnapshot, false, + &tmfd, false, + /* wal_logical */ + false); + + if (res != TM_Ok) + ereport(ERROR, (errmsg("failed to apply concurrent DELETE"))); pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_DELETED, 1); } @@ -2618,7 +2980,7 @@ apply_concurrent_delete(Relation rel, HeapTuple tup_target, */ static HeapTuple find_target_tuple(Relation rel, ScanKey key, int nkeys, HeapTuple tup_key, - IndexInsertState *iistate, + Snapshot snapshot, IndexInsertState *iistate, TupleTableSlot *ident_slot, IndexScanDesc *scan_p) { IndexScanDesc scan; @@ -2627,7 +2989,7 @@ find_target_tuple(Relation rel, ScanKey key, int nkeys, HeapTuple tup_key, HeapTuple result = NULL; /* XXX no instrumentation for now */ - scan = index_beginscan(rel, iistate->ident_index, GetActiveSnapshot(), + scan = index_beginscan(rel, iistate->ident_index, snapshot, NULL, nkeys, 0); *scan_p = scan; index_rescan(scan, key, nkeys, NULL, 0); @@ -2699,6 +3061,8 @@ process_concurrent_changes(LogicalDecodingContext *ctx, XLogRecPtr end_of_wal, } PG_FINALLY(); { + ResetRepackCurrentXids(); + if (rel_src) rel_dst->rd_toastoid = InvalidOid; } diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c index 00f7bbc5f59..25bb92b33f2 100644 --- a/src/backend/replication/logical/decode.c +++ b/src/backend/replication/logical/decode.c @@ -469,9 +469,18 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) SnapBuild *builder = ctx->snapshot_builder; /* - * Check if REPACK CONCURRENTLY is being performed by this backend. If so, - * only decode data changes of the table that it is processing, and the - * changes of its TOAST relation. + * If the change is not intended for logical decoding, do not even + * establish transaction for it. This is particularly important if the + * record was generated by REPACK CONCURRENTLY because this command uses + * the original XID when doing changes in the new storage. The decoding + * system probably does not expect to see the same transaction multiple + * times. + */ + + /* + * First, check if REPACK CONCURRENTLY is being performed by this backend. + * If so, only decode data changes of the table that it is processing, and + * the changes of its TOAST relation. * * (TOAST locator should not be set unless the main is.) */ @@ -491,6 +500,61 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) return; } + /* + * Second, skip records which do not contain sufficient information for + * the decoding. + * + * One particular problem we solve here is that REPACK CONCURRENTLY + * generates WAL when doing changes in the new table. Those changes should + * not be decoded because reorderbuffer.c considers their XID already + * committed. (REPACK CONCURRENTLY deliberately generates WAL records in + * such a way that they are skipped here.) + */ + switch (info) + { + case XLOG_HEAP_INSERT: + { + xl_heap_insert *rec; + + rec = (xl_heap_insert *) XLogRecGetData(buf->record); + + /* + * This does happen when 1) raw_heap_insert marks the TOAST + * record as HEAP_INSERT_NO_LOGICAL, 2) REPACK CONCURRENTLY + * replays inserts performed by other backends. + */ + if ((rec->flags & XLH_INSERT_CONTAINS_NEW_TUPLE) == 0) + return; + + break; + } + + case XLOG_HEAP_HOT_UPDATE: + case XLOG_HEAP_UPDATE: + { + xl_heap_update *rec; + + rec = (xl_heap_update *) XLogRecGetData(buf->record); + if ((rec->flags & + (XLH_UPDATE_CONTAINS_NEW_TUPLE | + XLH_UPDATE_CONTAINS_OLD_TUPLE | + XLH_UPDATE_CONTAINS_OLD_KEY)) == 0) + return; + + break; + } + + case XLOG_HEAP_DELETE: + { + xl_heap_delete *rec; + + rec = (xl_heap_delete *) XLogRecGetData(buf->record); + if (rec->flags & XLH_DELETE_NO_LOGICAL) + return; + break; + } + } + ReorderBufferProcessXid(ctx->reorder, xid, buf->origptr); /* @@ -923,13 +987,6 @@ DecodeInsert(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) xlrec = (xl_heap_insert *) XLogRecGetData(r); - /* - * Ignore insert records without new tuples (this does happen when - * raw_heap_insert marks the TOAST record as HEAP_INSERT_NO_LOGICAL). - */ - if (!(xlrec->flags & XLH_INSERT_CONTAINS_NEW_TUPLE)) - 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/snapbuild.c b/src/backend/replication/logical/snapbuild.c index c32e459411b..fde4955c328 100644 --- a/src/backend/replication/logical/snapbuild.c +++ b/src/backend/replication/logical/snapbuild.c @@ -155,7 +155,7 @@ static bool ExportInProgress = false; static void SnapBuildPurgeOlderTxn(SnapBuild *builder); /* snapshot building/manipulation/distribution functions */ -static Snapshot SnapBuildBuildSnapshot(SnapBuild *builder); +static Snapshot SnapBuildBuildSnapshot(SnapBuild *builder, XLogRecPtr lsn); static void SnapBuildFreeSnapshot(Snapshot snap); @@ -352,12 +352,17 @@ SnapBuildSnapDecRefcount(Snapshot snap) * Build a new snapshot, based on currently committed catalog-modifying * transactions. * + * 'lsn' is the location of the commit record (of a catalog-changing + * transaction) that triggered creation of the snapshot. Pass + * InvalidXLogRecPtr for the transaction base snapshot or if it the user of + * the snapshot should not need the LSN. + * * In-progress transactions with catalog access are *not* allowed to modify * these snapshots; they have to copy them and fill in appropriate ->curcid * and ->subxip/subxcnt values. */ static Snapshot -SnapBuildBuildSnapshot(SnapBuild *builder) +SnapBuildBuildSnapshot(SnapBuild *builder, XLogRecPtr lsn) { Snapshot snapshot; Size ssize; @@ -425,6 +430,7 @@ SnapBuildBuildSnapshot(SnapBuild *builder) snapshot->active_count = 0; snapshot->regd_count = 0; snapshot->snapXactCompletionCount = 0; + snapshot->lsn = lsn; return snapshot; } @@ -461,7 +467,7 @@ SnapBuildInitialSnapshot(SnapBuild *builder) if (TransactionIdIsValid(MyProc->xmin)) elog(ERROR, "cannot build an initial slot snapshot when MyProc->xmin already is valid"); - snap = SnapBuildBuildSnapshot(builder); + snap = SnapBuildBuildSnapshot(builder, InvalidXLogRecPtr); /* * We know that snap->xmin is alive, enforced by the logical xmin @@ -502,7 +508,7 @@ SnapBuildInitialSnapshotForRepack(SnapBuild *builder) Assert(builder->state == SNAPBUILD_CONSISTENT); - snap = SnapBuildBuildSnapshot(builder); + snap = SnapBuildBuildSnapshot(builder, InvalidXLogRecPtr); return SnapBuildMVCCFromHistoric(snap, false); } @@ -636,7 +642,7 @@ SnapBuildGetOrBuildSnapshot(SnapBuild *builder) /* only build a new snapshot if we don't have a prebuilt one */ if (builder->snapshot == NULL) { - builder->snapshot = SnapBuildBuildSnapshot(builder); + builder->snapshot = SnapBuildBuildSnapshot(builder, InvalidXLogRecPtr); /* increase refcount for the snapshot builder */ SnapBuildSnapIncRefcount(builder->snapshot); } @@ -716,7 +722,7 @@ SnapBuildProcessChange(SnapBuild *builder, TransactionId xid, XLogRecPtr lsn) /* only build a new snapshot if we don't have a prebuilt one */ if (builder->snapshot == NULL) { - builder->snapshot = SnapBuildBuildSnapshot(builder); + builder->snapshot = SnapBuildBuildSnapshot(builder, lsn); /* increase refcount for the snapshot builder */ SnapBuildSnapIncRefcount(builder->snapshot); } @@ -1085,7 +1091,7 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid, if (builder->snapshot) SnapBuildSnapDecRefcount(builder->snapshot); - builder->snapshot = SnapBuildBuildSnapshot(builder); + builder->snapshot = SnapBuildBuildSnapshot(builder, lsn); /* we might need to execute invalidations, add snapshot */ if (!ReorderBufferXidHasBaseSnapshot(builder->reorder, xid)) @@ -1910,7 +1916,7 @@ SnapBuildRestore(SnapBuild *builder, XLogRecPtr lsn) { SnapBuildSnapDecRefcount(builder->snapshot); } - builder->snapshot = SnapBuildBuildSnapshot(builder); + builder->snapshot = SnapBuildBuildSnapshot(builder, InvalidXLogRecPtr); SnapBuildSnapIncRefcount(builder->snapshot); ReorderBufferSetRestartPoint(builder->reorder, lsn); diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c index 687fbbc59bb..28bd16f9cc7 100644 --- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c +++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c @@ -32,7 +32,8 @@ static void plugin_truncate(struct LogicalDecodingContext *ctx, Relation relations[], ReorderBufferChange *change); static void store_change(LogicalDecodingContext *ctx, - ConcurrentChangeKind kind, HeapTuple tuple); + ConcurrentChangeKind kind, HeapTuple tuple, + TransactionId xid); void _PG_output_plugin_init(OutputPluginCallbacks *cb) @@ -100,6 +101,7 @@ plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, Relation relation, ReorderBufferChange *change) { RepackDecodingState *dstate; + Snapshot snapshot; dstate = (RepackDecodingState *) ctx->output_writer_private; @@ -107,6 +109,48 @@ plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (relation->rd_id != dstate->relid) return; + /* + * Catalog snapshot is fine because the table we are processing is + * temporarily considered a user catalog table. + */ + snapshot = GetCatalogSnapshot(InvalidOid); + Assert(snapshot->snapshot_type == SNAPSHOT_HISTORIC_MVCC); + Assert(!snapshot->suboverflowed); + + /* + * This should not happen, but if we don't have enough information to + * apply a new snapshot, the consequences would be bad. Thus prefer ERROR + * to Assert(). + */ + if (XLogRecPtrIsInvalid(snapshot->lsn)) + ereport(ERROR, (errmsg("snapshot has invalid LSN"))); + + /* + * reorderbuffer.c changes the catalog snapshot as soon as it sees a new + * CID or a commit record of a catalog-changing transaction. + */ + if (dstate->snapshot == NULL || snapshot->lsn != dstate->snapshot_lsn || + snapshot->curcid != dstate->snapshot->curcid) + { + /* CID should not go backwards. */ + Assert(dstate->snapshot == NULL || + snapshot->curcid >= dstate->snapshot->curcid || + change->txn->xid != dstate->last_change_xid); + + /* + * XXX Is it a problem that the copy is created in + * TopTransactionContext? + * + * XXX Wouldn't it be o.k. for SnapBuildMVCCFromHistoric() to set xcnt + * to 0 instead of converting xip in this case? The point is that + * transactions which are still in progress from the perspective of + * reorderbuffer.c could not be replayed yet, so we do not need to + * examine their XIDs. + */ + dstate->snapshot = SnapBuildMVCCFromHistoric(snapshot, false); + dstate->snapshot_lsn = snapshot->lsn; + } + /* Decode entry depending on its type */ switch (change->action) { @@ -124,7 +168,7 @@ plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (newtuple == NULL) elog(ERROR, "Incomplete insert info."); - store_change(ctx, CHANGE_INSERT, newtuple); + store_change(ctx, CHANGE_INSERT, newtuple, change->txn->xid); } break; case REORDER_BUFFER_CHANGE_UPDATE: @@ -141,9 +185,11 @@ plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, elog(ERROR, "Incomplete update info."); if (oldtuple != NULL) - store_change(ctx, CHANGE_UPDATE_OLD, oldtuple); + store_change(ctx, CHANGE_UPDATE_OLD, oldtuple, + change->txn->xid); - store_change(ctx, CHANGE_UPDATE_NEW, newtuple); + store_change(ctx, CHANGE_UPDATE_NEW, newtuple, + change->txn->xid); } break; case REORDER_BUFFER_CHANGE_DELETE: @@ -156,7 +202,7 @@ plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (oldtuple == NULL) elog(ERROR, "Incomplete delete info."); - store_change(ctx, CHANGE_DELETE, oldtuple); + store_change(ctx, CHANGE_DELETE, oldtuple, change->txn->xid); } break; default: @@ -190,13 +236,13 @@ plugin_truncate(struct LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (i == nrelations) return; - store_change(ctx, CHANGE_TRUNCATE, NULL); + store_change(ctx, CHANGE_TRUNCATE, NULL, InvalidTransactionId); } /* Store concurrent data change. */ static void store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind, - HeapTuple tuple) + HeapTuple tuple, TransactionId xid) { RepackDecodingState *dstate; char *change_raw; @@ -266,6 +312,11 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind, dst = dst_start + SizeOfConcurrentChange; memcpy(dst, tuple->t_data, tuple->t_len); + /* Initialize the other fields. */ + change.xid = xid; + change.snapshot = dstate->snapshot; + dstate->snapshot->active_count++; + /* The data has been copied. */ if (flattened) pfree(tuple); @@ -279,6 +330,9 @@ store: isnull[0] = false; tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change, values, isnull); +#ifdef USE_ASSERT_CHECKING + dstate->last_change_xid = xid; +#endif /* Accounting. */ dstate->nchanges++; diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index e9ddf39500c..e24e1795aa9 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -151,6 +151,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, InjectionPointShmemSize()); size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); + size = add_size(size, RepackShmemSize()); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -344,6 +345,7 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + RepackShmemInit(); } /* diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c index 4eb67720737..14eda1c24ee 100644 --- a/src/backend/utils/cache/inval.c +++ b/src/backend/utils/cache/inval.c @@ -1633,6 +1633,27 @@ CacheInvalidateRelcache(Relation relation) databaseId, relationId); } +/* + * CacheInvalidateRelcacheImmediate + * Send invalidation message for the specified relation's relcache entry. + * + * Currently this is used in REPACK CONCURRENTLY, to make sure that other + * backends are aware that the command is being executed for the relation. + */ +void +CacheInvalidateRelcacheImmediate(Oid relid) +{ + SharedInvalidationMessage msg; + + msg.rc.id = SHAREDINVALRELCACHE_ID; + msg.rc.dbId = MyDatabaseId; + msg.rc.relId = relid; + /* check AddCatcacheInvalidationMessage() for an explanation */ + VALGRIND_MAKE_MEM_DEFINED(&msg, sizeof(msg)); + + SendSharedInvalidMessages(&msg, 1); +} + /* * CacheInvalidateRelcacheAll * Register invalidation of the whole relcache at the end of command. diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index a495f22876d..679cc6be1d1 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -1253,6 +1253,10 @@ retry: /* make sure relation is marked as having no open file yet */ relation->rd_smgr = NULL; + /* Is REPACK CONCURRENTLY in progress? */ + relation->rd_repack_concurrent = + is_concurrent_repack_in_progress(targetRelId); + /* * now we can free the memory allocated for pg_class_tuple */ diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index bdeb2f83540..b0c6f1d916f 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -325,21 +325,24 @@ extern BulkInsertState GetBulkInsertState(void); extern void FreeBulkInsertState(BulkInsertState); extern void ReleaseBulkInsertStatePin(BulkInsertState bistate); -extern void heap_insert(Relation relation, HeapTuple tup, CommandId cid, - int options, BulkInsertState bistate); +extern void heap_insert(Relation relation, HeapTuple tup, TransactionId xid, + CommandId cid, int options, BulkInsertState bistate); extern void heap_multi_insert(Relation relation, struct TupleTableSlot **slots, int ntuples, CommandId cid, int options, BulkInsertState bistate); extern TM_Result heap_delete(Relation relation, ItemPointer tid, - CommandId cid, Snapshot crosscheck, bool wait, - struct TM_FailureData *tmfd, bool changingPart); + TransactionId xid, CommandId cid, + Snapshot crosscheck, bool wait, + struct TM_FailureData *tmfd, bool changingPart, + bool wal_logical); extern void heap_finish_speculative(Relation relation, ItemPointer tid); extern void heap_abort_speculative(Relation relation, ItemPointer tid); extern TM_Result heap_update(Relation relation, ItemPointer otid, - HeapTuple newtup, + HeapTuple newtup, TransactionId xid, CommandId cid, Snapshot crosscheck, bool wait, struct TM_FailureData *tmfd, LockTupleMode *lockmode, - TU_UpdateIndexes *update_indexes); + TU_UpdateIndexes *update_indexes, + bool wal_logical); extern TM_Result heap_lock_tuple(Relation relation, HeapTuple tuple, CommandId cid, LockTupleMode mode, LockWaitPolicy wait_policy, bool follow_updates, diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h index 277df6b3cf0..8d4af07f840 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/xact.h b/src/include/access/xact.h index b2bc10ee041..fbb66d559b6 100644 --- a/src/include/access/xact.h +++ b/src/include/access/xact.h @@ -482,6 +482,8 @@ extern Size EstimateTransactionStateSpace(void); extern void SerializeTransactionState(Size maxsize, char *start_address); extern void StartParallelWorkerTransaction(char *tstatespace); extern void EndParallelWorkerTransaction(void); +extern void SetRepackCurrentXids(TransactionId *xip, int xcnt); +extern void ResetRepackCurrentXids(void); extern bool IsTransactionBlock(void); extern bool IsTransactionOrTransactionBlock(void); extern char TransactionBlockStatusCode(void); diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h index 569cc2184b3..ab1d9fc25dc 100644 --- a/src/include/commands/cluster.h +++ b/src/include/commands/cluster.h @@ -73,6 +73,14 @@ typedef struct ConcurrentChange /* See the enum above. */ ConcurrentChangeKind kind; + /* Transaction that changes the data. */ + TransactionId xid; + + /* + * Historic catalog snapshot that was used to decode this change. + */ + Snapshot snapshot; + /* * The actual tuple. * @@ -104,6 +112,8 @@ typedef struct RepackDecodingState * tuplestore does this transparently. */ Tuplestorestate *tstore; + /* XID of the last change added to tstore. */ + TransactionId last_change_xid PG_USED_FOR_ASSERTS_ONLY; /* The current number of changes in tstore. */ double nchanges; @@ -124,6 +134,14 @@ typedef struct RepackDecodingState /* Slot to retrieve data from tstore. */ TupleTableSlot *tsslot; + /* + * Historic catalog snapshot that was used to decode the most recent + * change. + */ + Snapshot snapshot; + /* LSN of the record */ + XLogRecPtr snapshot_lsn; + ResourceOwner resowner; } RepackDecodingState; @@ -148,5 +166,9 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, MultiXactId cutoffMulti, char newrelpersistence); +extern Size RepackShmemSize(void); +extern void RepackShmemInit(void); +extern bool is_concurrent_repack_in_progress(Oid relid); + extern void repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel); #endif /* CLUSTER_H */ diff --git a/src/include/utils/inval.h b/src/include/utils/inval.h index 9b871caef62..ae9dee394dc 100644 --- a/src/include/utils/inval.h +++ b/src/include/utils/inval.h @@ -50,6 +50,8 @@ extern void CacheInvalidateCatalog(Oid catalogId); extern void CacheInvalidateRelcache(Relation relation); +extern void CacheInvalidateRelcacheImmediate(Oid relid); + extern void CacheInvalidateRelcacheAll(void); extern void CacheInvalidateRelcacheByTuple(HeapTuple classTuple); diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index d94fddd7cef..372065fc570 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -253,6 +253,9 @@ typedef struct RelationData bool pgstat_enabled; /* should relation stats be counted */ /* use "struct" here to avoid needing to include pgstat.h: */ struct PgStat_TableStatus *pgstat_info; /* statistics collection area */ + + /* Is REPACK CONCURRENTLY being performed on this relation? */ + bool rd_repack_concurrent; } RelationData; @@ -692,7 +695,9 @@ RelationCloseSmgr(Relation relation) #define RelationIsAccessibleInLogicalDecoding(relation) \ (XLogLogicalInfoActive() && \ RelationNeedsWAL(relation) && \ - (IsCatalogRelation(relation) || RelationIsUsedAsCatalogTable(relation))) + (IsCatalogRelation(relation) || \ + RelationIsUsedAsCatalogTable(relation) || \ + (relation)->rd_repack_concurrent)) /* * RelationIsLogicallyLogged diff --git a/src/include/utils/snapshot.h b/src/include/utils/snapshot.h index 0e546ec1497..014f27db7d7 100644 --- a/src/include/utils/snapshot.h +++ b/src/include/utils/snapshot.h @@ -13,6 +13,7 @@ #ifndef SNAPSHOT_H #define SNAPSHOT_H +#include "access/xlogdefs.h" #include "lib/pairingheap.h" @@ -201,6 +202,8 @@ typedef struct SnapshotData uint32 regd_count; /* refcount on RegisteredSnapshots */ pairingheap_node ph_node; /* link in the RegisteredSnapshots heap */ + XLogRecPtr lsn; /* position in the WAL stream when taken */ + /* * The transaction completion count at the time GetSnapshotData() built * this snapshot. Allows to avoid re-computing static snapshots when no diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e89db0a2ee7..e1e3e619c4b 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2510,6 +2510,7 @@ ReorderBufferTupleCidKey ReorderBufferUpdateProgressTxnCB ReorderTuple RepOriginId +RepackedRel RepackDecodingState RepackStmt ReparameterizeForeignPathByChild_function -- 2.43.5 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v12-0006-Add-regression-tests.patch From 5f61e2c87a5a70f100d4f12ff45e8c3fe629ba3e Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Tue, 1 Apr 2025 13:48:57 +0200 Subject: [PATCH 6/9] Add regression tests. As this patch series adds the CONCURRENTLY option to the REPACK command, it's appropriate to test that the "concurrent data changes" (i.e. changes done by application while we are copying the table contents to the new storage) are processed correctly. Injection points are used to stop the data copying at some point. While the backend in charge of the copying is waiting on the injection point, another backend runs some INSERT, UPDATE and DELETE commands on the table. Then we wake up the first backend and let the REPACK CONCURRENTLY command finish. Finally we check that all the "concurrent data changes" are present in the table and that they contain the correct visibility information. --- src/backend/commands/cluster.c | 7 + src/test/modules/injection_points/Makefile | 3 +- .../injection_points/expected/repack.out | 113 ++++++++++++++ .../modules/injection_points/logical.conf | 1 + src/test/modules/injection_points/meson.build | 4 + .../injection_points/specs/repack.spec | 140 ++++++++++++++++++ 6 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack.out create mode 100644 src/test/modules/injection_points/logical.conf create mode 100644 src/test/modules/injection_points/specs/repack.spec diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index e30c1a78e9c..3895013ca9d 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -59,6 +59,7 @@ #include "utils/formatting.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" @@ -3286,6 +3287,12 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, */ ident_key = build_identity_key(ident_idx_new, OldHeap, &ident_key_nentries); + /* + * During testing, wait for another backend to perform concurrent data + * changes which we will process below. + */ + INJECTION_POINT("repack-concurrently-before-lock"); + /* * Flush all WAL records inserted so far (possibly except for the last * incomplete page, see GetInsertRecPtr), to minimize the amount of data diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index e680991f8d4..405d0811b4f 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -14,7 +14,8 @@ PGFILEDESC = "injection_points - facility for injection points" REGRESS = injection_points hashagg reindex_conc REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress -ISOLATION = basic inplace syscache-update-pruned +ISOLATION = basic inplace syscache-update-pruned repack +ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf TAP_TESTS = 1 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..49a736ed617 --- /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(_xmin, _cmin, i, j) + SELECT xmin, cmin, 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(_xmin, _cmin, i, j) + SELECT xmin, cmin, i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (_xmin, _cmin, 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/logical.conf b/src/test/modules/injection_points/logical.conf new file mode 100644 index 00000000000..c8f264bc6cb --- /dev/null +++ b/src/test/modules/injection_points/logical.conf @@ -0,0 +1 @@ +wal_level = logical \ No newline at end of file diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index d61149712fd..0e3c47ba999 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -46,9 +46,13 @@ tests += { 'specs': [ 'basic', 'inplace', + 'repack', 'syscache-update-pruned', ], 'runningcheck': false, # see syscache-update-pruned + # 'repack' requires wal_level = 'logical'. + 'regress_args': ['--temp-config', files('logical.conf')], + }, 'tap': { 'env': { 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..5aa8983f98d --- /dev/null +++ b/src/test/modules/injection_points/specs/repack.spec @@ -0,0 +1,140 @@ +# Prefix the system columns with underscore as they are not allowed as column +# names. +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(_xmin xid, _cmin cid, i int, j int); + CREATE TABLE data_s2(_xmin xid, _cmin cid, 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. +# +# xmin and cmin columns are used to check that we do not change tuple +# visibility information. Since we do not expect xmin to stay unchanged across +# test runs, it cannot appear in the output text. Instead, 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(_xmin, _cmin, i, j) + SELECT xmin, cmin, i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (_xmin, _cmin, 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. +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. +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(_xmin, _cmin, i, j) + SELECT xmin, cmin, 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 -- 2.43.5 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v12-0007-Introduce-repack_max_xlock_time-configuration-variab.patch From 15c63e21fa5fc312c0e9a40211f781555445c06f Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Tue, 1 Apr 2025 13:48:57 +0200 Subject: [PATCH 7/9] Introduce repack_max_xlock_time configuration variable. When executing REPACK CONCURRENTLY, we need the AccessExclusiveLock to swap the relation files and that should require pretty short time. However, on a busy system, other backends might change non-negligible amount of data in the table while we are waiting for the lock. Since these changes must be applied to the new storage before the swap, the time we eventually hold the lock might become non-negligible too. If the user is worried about this situation, he can set repack_max_xlock_time to the maximum time for which the exclusive lock may be held. If this amount of time is not sufficient to complete the REPACK CONCURRENTLY command, ERROR is raised and the command is canceled. --- doc/src/sgml/config.sgml | 31 ++++ doc/src/sgml/ref/repack.sgml | 9 +- src/backend/access/heap/heapam_handler.c | 3 +- src/backend/commands/cluster.c | 135 +++++++++++++++--- src/backend/utils/misc/guc_tables.c | 15 +- src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/commands/cluster.h | 5 +- .../injection_points/expected/repack.out | 74 +++++++++- .../injection_points/specs/repack.spec | 42 ++++++ 9 files changed, 294 insertions(+), 21 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index fea683cb49c..d1fc39b98be 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -11190,6 +11190,37 @@ dynamic_library_path = '/usr/local/lib/postgresql:$libdir' + + repack_max_xlock_time (integer) + + repack_max_xlock_time configuration parameter + + + + + This is the maximum amount of time to hold an exclusive lock on a + table by REPACK with + the CONCURRENTLY option. Typically, these commands + should not need the lock for longer time + than TRUNCATE does. However, additional time might + be needed if the system is too busy. (See + for explanation how the CONCURRENTLY option works.) + + + + If you want to restrict the lock time, set this variable to the + highest acceptable value. If it appears during the processing that + additional time is needed to release the lock, the command will be + cancelled. + + + + The default value is 0, which means that the lock is not released + until the concurrent data changes are processed. + + + + diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml index 9ee640e3517..0c250689d13 100644 --- a/doc/src/sgml/ref/repack.sgml +++ b/doc/src/sgml/ref/repack.sgml @@ -188,7 +188,14 @@ REPACK [ ( option [, ...] ) ] CONCU () 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. + 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. If you are + worried about this situation, set + the repack_max_xlock_time + configuration parameter to a value that your applications can tolerate. diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index ea1d6f299b3..850708c7830 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -1008,7 +1008,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, end_of_wal = GetFlushRecPtr(NULL); if ((end_of_wal - end_of_wal_prev) > wal_segment_size) { - repack_decode_concurrent_changes(decoding_ctx, end_of_wal); + repack_decode_concurrent_changes(decoding_ctx, end_of_wal, + NULL); end_of_wal_prev = end_of_wal; } } diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index 3895013ca9d..085649716ac 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -17,6 +17,8 @@ */ #include "postgres.h" +#include + #include "access/amapi.h" #include "access/heapam.h" #include "access/multixact.h" @@ -103,6 +105,15 @@ static Oid repacked_rel = InvalidOid; RelFileLocator repacked_rel_locator = {.relNumber = InvalidOid}; RelFileLocator repacked_rel_toast_locator = {.relNumber = InvalidOid}; +/* + * The maximum time to hold AccessExclusiveLock during the final + * processing. Note that only the execution time of + * process_concurrent_changes() is included here. The very last steps like + * swap_relation_files() shouldn't get blocked and it'd be wrong to consider + * them a reason to abort otherwise completed processing. + */ +int repack_max_xlock_time = 0; + /* * Everything we need to call ExecInsertIndexTuples(). */ @@ -149,7 +160,8 @@ static LogicalDecodingContext *setup_logical_decoding(Oid relid, static HeapTuple get_changed_tuple(char *change); static void apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, ScanKey key, int nkeys, - IndexInsertState *iistate); + IndexInsertState *iistate, + struct timeval *must_complete); static void apply_concurrent_insert(Relation rel, ConcurrentChange *change, HeapTuple tup, IndexInsertState *iistate, TupleTableSlot *index_slot); @@ -166,13 +178,15 @@ static HeapTuple find_target_tuple(Relation rel, ScanKey key, int nkeys, IndexInsertState *iistate, TupleTableSlot *ident_slot, IndexScanDesc *scan_p); -static void process_concurrent_changes(LogicalDecodingContext *ctx, +static bool process_concurrent_changes(LogicalDecodingContext *ctx, XLogRecPtr end_of_wal, Relation rel_dst, Relation rel_src, ScanKey ident_key, int ident_key_nentries, - IndexInsertState *iistate); + IndexInsertState *iistate, + struct timeval *must_complete); +static bool processing_time_elapsed(struct timeval *must_complete); static IndexInsertState *get_index_insert_state(Relation relation, Oid ident_index_id); static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src, @@ -2598,7 +2612,8 @@ get_changed_tuple(char *change) */ void repack_decode_concurrent_changes(LogicalDecodingContext *ctx, - XLogRecPtr end_of_wal) + XLogRecPtr end_of_wal, + struct timeval *must_complete) { RepackDecodingState *dstate; ResourceOwner resowner_old; @@ -2628,6 +2643,9 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx, if (record != NULL) LogicalDecodingProcessRecord(ctx, ctx->reader); + if (processing_time_elapsed(must_complete)) + break; + /* * If WAL segment boundary has been crossed, inform the decoding * system that the catalog_xmin can advance. (We can confirm more @@ -2668,7 +2686,8 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx, */ static void apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, - ScanKey key, int nkeys, IndexInsertState *iistate) + ScanKey key, int nkeys, IndexInsertState *iistate, + struct timeval *must_complete) { TupleTableSlot *index_slot, *ident_slot; @@ -2699,6 +2718,9 @@ apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, CHECK_FOR_INTERRUPTS(); + Assert(dstate->nchanges > 0); + dstate->nchanges--; + /* Get the change from the single-column tuple. */ tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree); heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull); @@ -2823,10 +2845,22 @@ apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */ Assert(shouldFree); pfree(tup_change); + + /* + * If there is a limit on the time of completion, check it now. + * However, make sure the loop does not break if tup_old was set in + * the previous iteration. In such a case we could not resume the + * processing in the next call. + */ + if (must_complete && tup_old == NULL && + processing_time_elapsed(must_complete)) + /* The next call will process the remaining changes. */ + break; } - tuplestore_clear(dstate->tstore); - dstate->nchanges = 0; + /* If we could not apply all the changes, the next call will do. */ + if (dstate->nchanges == 0) + tuplestore_clear(dstate->tstore); /* Cleanup. */ ExecDropSingleTupleTableSlot(index_slot); @@ -3030,11 +3064,15 @@ find_target_tuple(Relation rel, ScanKey key, int nkeys, HeapTuple tup_key, * Decode and apply concurrent changes. * * Pass rel_src iff its reltoastrelid is needed. + * + * Returns true if must_complete is NULL or if managed to complete by the time + * *must_complete indicates. */ -static void +static bool process_concurrent_changes(LogicalDecodingContext *ctx, XLogRecPtr end_of_wal, Relation rel_dst, Relation rel_src, ScanKey ident_key, - int ident_key_nentries, IndexInsertState *iistate) + int ident_key_nentries, IndexInsertState *iistate, + struct timeval *must_complete) { RepackDecodingState *dstate; @@ -3043,10 +3081,19 @@ process_concurrent_changes(LogicalDecodingContext *ctx, XLogRecPtr end_of_wal, dstate = (RepackDecodingState *) ctx->output_writer_private; - repack_decode_concurrent_changes(ctx, end_of_wal); + repack_decode_concurrent_changes(ctx, end_of_wal, must_complete); + if (processing_time_elapsed(must_complete)) + /* Caller is responsible for applying the changes. */ + return false; + + /* + * *must_complete not reached, so there are really no changes. (It's + * possible to see no changes just because not enough time was left for + * the decoding.) + */ if (dstate->nchanges == 0) - return; + return true; PG_TRY(); { @@ -3058,7 +3105,7 @@ process_concurrent_changes(LogicalDecodingContext *ctx, XLogRecPtr end_of_wal, rel_dst->rd_toastoid = rel_src->rd_rel->reltoastrelid; apply_concurrent_changes(dstate, rel_dst, ident_key, - ident_key_nentries, iistate); + ident_key_nentries, iistate, must_complete); } PG_FINALLY(); { @@ -3068,6 +3115,28 @@ process_concurrent_changes(LogicalDecodingContext *ctx, XLogRecPtr end_of_wal, rel_dst->rd_toastoid = InvalidOid; } PG_END_TRY(); + + /* + * apply_concurrent_changes() does check the processing time, so if some + * changes are left, we ran out of time. + */ + return dstate->nchanges == 0; +} + +/* + * Check if the current time is beyond *must_complete. + */ +static bool +processing_time_elapsed(struct timeval *must_complete) +{ + struct timeval now; + + if (must_complete == NULL) + return false; + + gettimeofday(&now, NULL); + + return timercmp(&now, must_complete, >); } static IndexInsertState * @@ -3229,6 +3298,8 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, Relation *ind_refs, *ind_refs_p; int nind; + struct timeval t_end; + struct timeval *t_end_ptr = NULL; /* Like in cluster_rel(). */ lockmode_old = ShareUpdateExclusiveLock; @@ -3309,7 +3380,8 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, */ process_concurrent_changes(ctx, end_of_wal, NewHeap, swap_toast_by_content ? OldHeap : NULL, - ident_key, ident_key_nentries, iistate); + ident_key, ident_key_nentries, iistate, + NULL); /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there @@ -3397,9 +3469,40 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, end_of_wal = GetFlushRecPtr(NULL); /* Apply the concurrent changes again. */ - process_concurrent_changes(ctx, end_of_wal, NewHeap, - swap_toast_by_content ? OldHeap : NULL, - ident_key, ident_key_nentries, iistate); + + /* + * This time we have the exclusive lock on the table, so make sure that + * repack_max_xlock_time is not exceeded. + */ + if (repack_max_xlock_time > 0) + { + int64 usec; + struct timeval t_start; + + gettimeofday(&t_start, NULL); + /* Add the whole seconds. */ + t_end.tv_sec = t_start.tv_sec + repack_max_xlock_time / 1000; + /* Add the rest, expressed in microseconds. */ + usec = t_start.tv_usec + 1000 * (repack_max_xlock_time % 1000); + /* The number of microseconds could have overflown. */ + t_end.tv_sec += usec / USECS_PER_SEC; + t_end.tv_usec = usec % USECS_PER_SEC; + t_end_ptr = &t_end; + } + + /* + * During testing, stop here to simulate excessive processing time. + */ + INJECTION_POINT("repack-concurrently-after-lock"); + + if (!process_concurrent_changes(ctx, end_of_wal, NewHeap, + swap_toast_by_content ? OldHeap : NULL, + ident_key, ident_key_nentries, iistate, + t_end_ptr)) + ereport(ERROR, + (errmsg("could not process concurrent data changes in time"), + errhint("Please consider adjusting \"repack_max_xlock_time\"."))); + /* Remember info about rel before closing OldHeap */ relpersistence = OldHeap->rd_rel->relpersistence; diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 4eaeca89f2c..533fd40d383 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -39,8 +39,9 @@ #include "catalog/namespace.h" #include "catalog/storage.h" #include "commands/async.h" -#include "commands/extension.h" +#include "commands/cluster.h" #include "commands/event_trigger.h" +#include "commands/extension.h" #include "commands/tablespace.h" #include "commands/trigger.h" #include "commands/user.h" @@ -2827,6 +2828,18 @@ struct config_int ConfigureNamesInt[] = 1600000000, 0, 2100000000, NULL, NULL, NULL }, + { + {"repack_max_xlock_time", PGC_USERSET, LOCK_MANAGEMENT, + gettext_noop("Maximum time for REPACK CONCURRENTLY to keep table locked."), + gettext_noop("The table is locked in exclusive mode during the final stage of processing. " + "If the lock time exceeds this value, error is raised and the lock is " + "released. Set to zero if you don't care how long the lock can be held."), + GUC_UNIT_MS + }, + &repack_max_xlock_time, + 0, 0, INT_MAX, + NULL, NULL, NULL + }, /* * See also CheckRequiredParameterValues() if this parameter changes diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index ff56a1f0732..bc0217161ec 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -763,6 +763,7 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate #lock_timeout = 0 # in milliseconds, 0 is disabled #idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled #idle_session_timeout = 0 # in milliseconds, 0 is disabled +#repack_max_xlock_time = 0 #bytea_output = 'hex' # hex, escape #xmlbinary = 'base64' #xmloption = 'content' diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h index ab1d9fc25dc..be283c70fce 100644 --- a/src/include/commands/cluster.h +++ b/src/include/commands/cluster.h @@ -59,6 +59,8 @@ typedef enum ClusterCommand extern RelFileLocator repacked_rel_locator; extern RelFileLocator repacked_rel_toast_locator; +extern PGDLLIMPORT int repack_max_xlock_time; + typedef enum { CHANGE_INSERT, @@ -153,7 +155,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid, ClusterCommand cmd); extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal); extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx, - XLogRecPtr end_of_wal); + XLogRecPtr end_of_wal, + struct timeval *must_complete); extern Oid make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod, char relpersistence, LOCKMODE lockmode); extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, diff --git a/src/test/modules/injection_points/expected/repack.out b/src/test/modules/injection_points/expected/repack.out index 49a736ed617..f2728d94222 100644 --- a/src/test/modules/injection_points/expected/repack.out +++ b/src/test/modules/injection_points/expected/repack.out @@ -1,4 +1,4 @@ -Parsed test spec with 2 sessions +Parsed test spec with 4 sessions starting permutation: wait_before_lock change_existing change_new change_subxact1 change_subxact2 check2 wakeup_before_lock check1 injection_points_attach @@ -111,3 +111,75 @@ injection_points_detach (1 row) +injection_points_detach +----------------------- + +(1 row) + + +starting permutation: wait_after_lock wakeup_after_lock +injection_points_attach +----------------------- + +(1 row) + +step wait_after_lock: + REPACK CONCURRENTLY repack_test USING INDEX repack_test_pkey; + +step wakeup_after_lock: + SELECT injection_points_wakeup('repack-concurrently-after-lock'); + +injection_points_wakeup +----------------------- + +(1 row) + +step wait_after_lock: <... completed> +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + + +starting permutation: wait_after_lock after_lock_delay wakeup_after_lock +injection_points_attach +----------------------- + +(1 row) + +step wait_after_lock: + REPACK CONCURRENTLY repack_test USING INDEX repack_test_pkey; + +step after_lock_delay: + SELECT pg_sleep(1.5); + +pg_sleep +-------- + +(1 row) + +step wakeup_after_lock: + SELECT injection_points_wakeup('repack-concurrently-after-lock'); + +injection_points_wakeup +----------------------- + +(1 row) + +step wait_after_lock: <... completed> +ERROR: could not process concurrent data changes in time +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec index 5aa8983f98d..0f45f9d2544 100644 --- a/src/test/modules/injection_points/specs/repack.spec +++ b/src/test/modules/injection_points/specs/repack.spec @@ -127,6 +127,34 @@ step wakeup_before_lock SELECT injection_points_wakeup('repack-concurrently-before-lock'); } +session s3 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('repack-concurrently-after-lock', 'wait'); + SET repack_max_xlock_time TO '1s'; +} +# Perform the initial load, lock the table in exclusive mode and wait. s4 will +# cancel the waiting. +step wait_after_lock +{ + REPACK CONCURRENTLY repack_test USING INDEX repack_test_pkey; +} +teardown +{ + SELECT injection_points_detach('repack-concurrently-after-lock'); +} + +session s4 +step wakeup_after_lock +{ + SELECT injection_points_wakeup('repack-concurrently-after-lock'); +} +step after_lock_delay +{ + SELECT pg_sleep(1.5); +} + # Test if data changes introduced while one session is performing REPACK # CONCURRENTLY find their way into the table. permutation @@ -138,3 +166,17 @@ permutation check2 wakeup_before_lock check1 + +# Test the repack_max_xlock_time configuration variable. +# +# First, cancel waiting on the injection point immediately. That way, REPACK +# should complete. +permutation + wait_after_lock + wakeup_after_lock +# Second, cancel the waiting with a delay that violates +# repack_max_xlock_time. +permutation + wait_after_lock + after_lock_delay + wakeup_after_lock -- 2.43.5 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v12-0008-Enable-logical-decoding-transiently-only-for-REPACK-.patch From 6e2f23e1f8c22b03bbfb19fbc1ae510286cb9801 Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Tue, 1 Apr 2025 13:48:57 +0200 Subject: [PATCH 8/9] Enable logical decoding transiently, only for REPACK CONCURRENTLY. As REPACK CONCURRENTLY uses logical decoding, it requires wal_level to be set to 'logical', while 'replica' is the default value. If logical replication is not used, users will probably be reluctant to set the GUC to 'logical' because it can affect server performance (by writing additional information to WAL) and because it cannot be changed to 'logical' only for the time REPACK CONCURRENTLY is running: change of this GUC requires server restart to take effect. This patch teaches postgres backend to recognize whether it should consider wal_level='logical' "locally" for particular transaction, even if the wal_level GUC is actually set to 'replica'. Also it ensures that the logical decoding specific information is added to WAL only for the tables which are currently being processed by REPACK CONCURRENTLY. If the logical decoding is enabled this way, only temporary replication slots should be created. The problem of permanent slot is that it is restored during server restart, and the restore fails if wal_level is not "globally" 'logical'. There is an independent work in progres to enable logical decoding transiently [1]. ISTM that this is too "heavyweight" solution for our problem. And I think that these two approaches are not mutually exclusive: once [1] is committed, we only need to adjust the XLogLogicalInfoActive() macro. [1] https://www.postgresql.org/message-id/CAD21AoCVLeLYq09pQPaWs%2BJwdni5FuJ8v2jgq-u9_uFbcp6UbA%40mail.gmail.com --- src/backend/access/transam/parallel.c | 8 ++ src/backend/access/transam/xact.c | 106 +++++++++++++++--- src/backend/access/transam/xlog.c | 1 + src/backend/commands/cluster.c | 94 +++++++++++++--- src/backend/replication/logical/logical.c | 9 +- src/backend/storage/ipc/standby.c | 4 +- src/include/access/xlog.h | 15 ++- src/include/commands/cluster.h | 1 + src/include/utils/rel.h | 6 +- src/test/modules/injection_points/Makefile | 1 - .../modules/injection_points/logical.conf | 1 - src/test/modules/injection_points/meson.build | 3 - src/tools/pgindent/typedefs.list | 1 + 13 files changed, 206 insertions(+), 44 deletions(-) delete mode 100644 src/test/modules/injection_points/logical.conf diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c index 94db1ec3012..a33318ea7bd 100644 --- a/src/backend/access/transam/parallel.c +++ b/src/backend/access/transam/parallel.c @@ -98,6 +98,7 @@ typedef struct FixedParallelState TimestampTz xact_ts; TimestampTz stmt_ts; SerializableXactHandle serializable_xact_handle; + int wal_level_transient; /* Mutex protects remaining fields. */ slock_t mutex; @@ -355,6 +356,7 @@ InitializeParallelDSM(ParallelContext *pcxt) fps->xact_ts = GetCurrentTransactionStartTimestamp(); fps->stmt_ts = GetCurrentStatementStartTimestamp(); fps->serializable_xact_handle = ShareSerializableXact(); + fps->wal_level_transient = wal_level_transient; SpinLockInit(&fps->mutex); fps->last_xlog_end = 0; shm_toc_insert(pcxt->toc, PARALLEL_KEY_FIXED, fps); @@ -1550,6 +1552,12 @@ ParallelWorkerMain(Datum main_arg) /* Attach to the leader's serializable transaction, if SERIALIZABLE. */ AttachSerializableXact(fps->serializable_xact_handle); + /* + * Restore the information whether this worker should behave as if + * wal_level was WAL_LEVEL_LOGICAL.. + */ + wal_level_transient = fps->wal_level_transient; + /* * We've initialized all of our state now; nothing should change * hereafter. diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 3db4cac030e..608dc5c79bb 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -36,6 +36,7 @@ #include "catalog/pg_enum.h" #include "catalog/storage.h" #include "commands/async.h" +#include "commands/cluster.h" #include "commands/tablecmds.h" #include "commands/trigger.h" #include "common/pg_prng.h" @@ -138,6 +139,12 @@ static TransactionId *ParallelCurrentXids; static int nRepackCurrentXids = 0; static TransactionId *RepackCurrentXids = NULL; +/* + * Have we determined the value of wal_level_transient for the current + * transaction? + */ +static bool wal_level_transient_checked = false; + /* * Miscellaneous flag bits to record events which occur on the top level * transaction. These flags are only persisted in MyXactFlags and are intended @@ -650,6 +657,7 @@ AssignTransactionId(TransactionState s) bool isSubXact = (s->parent != NULL); ResourceOwner currentOwner; bool log_unknown_top = false; + bool set_wal_level_transient = false; /* Assert that caller didn't screw up */ Assert(!FullTransactionIdIsValid(s->fullTransactionId)); @@ -664,6 +672,32 @@ AssignTransactionId(TransactionState s) (errcode(ERRCODE_INVALID_TRANSACTION_STATE), errmsg("cannot assign transaction IDs during a parallel operation"))); + /* + * The first call (i.e. the first write) in the transaction tree + * determines whether the whole transaction assumes logical decoding or + * not. + */ + if (!wal_level_transient_checked) + { + Assert(wal_level_transient == WAL_LEVEL_MINIMAL); + + /* + * Do not repeat the check when calling this function for parent + * transactions. + */ + wal_level_transient_checked = true; + + /* + * Remember that the actual check is needed. We cannot do it until the + * top-level transaction has its XID assigned, see comments below. + * + * There is no use case for overriding MINIMAL, and LOGICAL cannot be + * overridden as such. + */ + if (wal_level == WAL_LEVEL_REPLICA) + set_wal_level_transient = true; + } + /* * Ensure parent(s) have XIDs, so that a child always has an XID later * than its parent. Mustn't recurse here, or we might get a stack @@ -693,20 +727,6 @@ AssignTransactionId(TransactionState s) pfree(parents); } - /* - * When wal_level=logical, guarantee that a subtransaction's xid can only - * be seen in the WAL stream if its toplevel xid has been logged before. - * If necessary we log an xact_assignment record with fewer than - * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set - * for a transaction even though it appears in a WAL record, we just might - * superfluously log something. That can happen when an xid is included - * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in - * xl_standby_locks. - */ - if (isSubXact && XLogLogicalInfoActive() && - !TopTransactionStateData.didLogXid) - log_unknown_top = true; - /* * Generate a new FullTransactionId and record its xid in PGPROC and * pg_subtrans. @@ -731,6 +751,54 @@ AssignTransactionId(TransactionState s) if (!isSubXact) RegisterPredicateLockingXid(XidFromFullTransactionId(s->fullTransactionId)); + /* + * Check if this transaction should consider wal_level=logical. + * + * Sometimes we need to turn on the logical decoding transiently although + * wal_level=WAL_LEVEL_REPLICA. Currently we do so when at least one table + * is being clustered concurrently, i.e. when we should assume that + * changes done by this transaction will be decoded. In such a case we + * adjust the value of XLogLogicalInfoActive() by setting + * wal_level_transient to LOGICAL. + * + * It's important not to do this check until the XID of the top-level + * transaction is in ProcGlobal: if the decoding becomes mandatory right + * after the check, our transaction will fail to write the necessary + * information to WAL. However, if the top-level transaction is already in + * ProcGlobal, its XID is guaranteed to appear in the xl_running_xacts + * record and therefore the snapshot builder will not try to decode the + * transaction (because it assumes it could have missed the initial part + * of the transaction). + * + * On the other hand, if the decoding became mandatory between the actual + * XID assignment and now, the transaction will WAL the decoding specific + * information unnecessarily. Let's assume that such race conditions do + * not happen too often. + */ + if (set_wal_level_transient) + { + /* + * Check for the operation that enables the logical decoding + * transiently. + */ + if (is_concurrent_repack_in_progress(InvalidOid)) + wal_level_transient = WAL_LEVEL_LOGICAL; + } + + /* + * When wal_level=logical, guarantee that a subtransaction's xid can only + * be seen in the WAL stream if its toplevel xid has been logged before. + * If necessary we log an xact_assignment record with fewer than + * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set + * for a transaction even though it appears in a WAL record, we just might + * superfluously log something. That can happen when an xid is included + * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in + * xl_standby_locks. + */ + if (isSubXact && XLogLogicalInfoActive() && + !TopTransactionStateData.didLogXid) + log_unknown_top = true; + /* * Acquire lock on the transaction XID. (We assume this cannot block.) We * have to ensure that the lock is assigned to the transaction's own @@ -2245,6 +2313,16 @@ StartTransaction(void) if (TransactionTimeout > 0) enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout); + /* + * wal_level_transient can override wal_level for individual transactions, + * which effectively enables logical decoding for them. At the moment we + * don't know if this transaction will write any data changes to be + * decoded. Should it do, AssignTransactionId() will check if the decoding + * needs to be considered. + */ + wal_level_transient = WAL_LEVEL_MINIMAL; + wal_level_transient_checked = false; + ShowTransactionState("StartTransaction"); } diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index fc30a52d496..ba758deefb4 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -129,6 +129,7 @@ bool wal_recycle = true; bool log_checkpoints = true; int wal_sync_method = DEFAULT_WAL_SYNC_METHOD; int wal_level = WAL_LEVEL_REPLICA; +int wal_level_transient = WAL_LEVEL_MINIMAL; int CommitDelay = 0; /* precommit delay in microseconds */ int CommitSiblings = 5; /* # concurrent xacts needed to sleep */ int wal_retrieve_retry_interval = 5000; diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index 085649716ac..c2201b046bc 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -2204,7 +2204,16 @@ typedef struct RepackedRel Oid dbid; } RepackedRel; -static HTAB *RepackedRelsHash = NULL; +typedef struct RepackedRels +{ + /* Hashtable of RepackedRel elements. */ + HTAB *hashtable; + + /* The number of elements in the hashtable.. */ + pg_atomic_uint32 nrels; +} RepackedRels; + +static RepackedRels *repackedRels = NULL; /* * Maximum number of entries in the hashtable. @@ -2217,22 +2226,38 @@ static HTAB *RepackedRelsHash = NULL; Size RepackShmemSize(void) { - return hash_estimate_size(MAX_REPACKED_RELS, sizeof(RepackedRel)); + Size result; + + result = sizeof(RepackedRels); + + result += hash_estimate_size(MAX_REPACKED_RELS, sizeof(RepackedRel)); + return result; } void RepackShmemInit(void) { + bool found; HASHCTL info; + repackedRels = ShmemInitStruct("Repacked Relations", + sizeof(RepackedRels), + &found); + if (!IsUnderPostmaster) + { + Assert(!found); + pg_atomic_init_u32(&repackedRels->nrels, 0); + } + else + Assert(found); + info.keysize = sizeof(RepackedRel); info.entrysize = info.keysize; - - RepackedRelsHash = ShmemInitHash("Repacked Relations", - MAX_REPACKED_RELS, - MAX_REPACKED_RELS, - &info, - HASH_ELEM | HASH_BLOBS); + repackedRels->hashtable = ShmemInitHash("Repacked Relations Hash", + MAX_REPACKED_RELS, + MAX_REPACKED_RELS, + &info, + HASH_ELEM | HASH_BLOBS); } /* @@ -2267,13 +2292,14 @@ begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p) *entry; bool found; static bool before_shmem_exit_callback_setup = false; + uint32 nrels PG_USED_FOR_ASSERTS_ONLY; relid = RelationGetRelid(rel); index = index_p ? *index_p : NULL; /* - * Make sure that we do not leave an entry in RepackedRelsHash if exiting - * due to FATAL. + * Make sure that we do not leave an entry in repackedRels->hashtable if + * exiting due to FATAL. */ if (!before_shmem_exit_callback_setup) { @@ -2288,7 +2314,7 @@ begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p) *entered_p = false; LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE); entry = (RepackedRel *) - hash_search(RepackedRelsHash, &key, HASH_ENTER_NULL, &found); + hash_search(repackedRels->hashtable, &key, HASH_ENTER_NULL, &found); if (found) { /* @@ -2306,9 +2332,13 @@ begin_concurrent_repack(Relation rel, Relation *index_p, bool *entered_p) (errmsg("too many requests for REPACK CONCURRENTLY at a time")), (errhint("Please consider increasing the \"max_replication_slots\" configuration parameter."))); + /* Increment the number of relations. */ + nrels = pg_atomic_fetch_add_u32(&repackedRels->nrels, 1); + Assert(nrels < MAX_REPACKED_RELS); + /* - * Even if anything fails below, the caller has to do cleanup in the - * shared memory. + * Even if the insertion of TOAST relid should fail below, the caller has + * to do cleanup. */ *entered_p = true; @@ -2390,6 +2420,7 @@ end_concurrent_repack(bool error) RepackedRel key; RepackedRel *entry = NULL; Oid relid = repacked_rel; + uint32 nrels PG_USED_FOR_ASSERTS_ONLY; /* Remove the relation from the hash if we managed to insert one. */ if (OidIsValid(repacked_rel)) @@ -2398,7 +2429,8 @@ end_concurrent_repack(bool error) key.relid = repacked_rel; key.dbid = MyDatabaseId; LWLockAcquire(RepackedRelsLock, LW_EXCLUSIVE); - entry = hash_search(RepackedRelsHash, &key, HASH_REMOVE, NULL); + entry = hash_search(repackedRels->hashtable, &key, HASH_REMOVE, + NULL); LWLockRelease(RepackedRelsLock); /* @@ -2427,6 +2459,10 @@ end_concurrent_repack(bool error) * cluster_before_shmem_exit_callback(). */ repacked_rel = InvalidOid; + + /* Decrement the number of relations. */ + nrels = pg_atomic_fetch_sub_u32(&repackedRels->nrels, 1); + Assert(nrels > 0); } /* @@ -2479,6 +2515,8 @@ cluster_before_shmem_exit_callback(int code, Datum arg) /* * Check if relation is currently being processed by REPACK CONCURRENTLY. + * + * If relid is InvalidOid, check if any relation is being processed. */ bool is_concurrent_repack_in_progress(Oid relid) @@ -2486,18 +2524,40 @@ is_concurrent_repack_in_progress(Oid relid) RepackedRel key, *entry; + /* + * If the caller is interested whether any relation is being repacked, + * just use the counter. + */ + if (!OidIsValid(relid)) + { + if (pg_atomic_read_u32(&repackedRels->nrels) > 0) + return true; + else + return false; + } + + /* For particular relation we need to search in the hashtable. */ memset(&key, 0, sizeof(key)); key.relid = relid; key.dbid = MyDatabaseId; LWLockAcquire(RepackedRelsLock, LW_SHARED); entry = (RepackedRel *) - hash_search(RepackedRelsHash, &key, HASH_FIND, NULL); + hash_search(repackedRels->hashtable, &key, HASH_FIND, NULL); LWLockRelease(RepackedRelsLock); return entry != NULL; } +/* + * Is this backend performing REPACK CONCURRENTLY? + */ +bool +is_concurrent_repack_run_by_me(void) +{ + return OidIsValid(repacked_rel); +} + /* * 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 @@ -2525,8 +2585,8 @@ setup_logical_decoding(Oid relid, const char *slotname, TupleDesc tupdesc) * useful for us. * * Regarding the value of need_full_snapshot, we pass false because the - * table we are processing is present in RepackedRelsHash and therefore, - * regarding logical decoding, treated like a catalog. + * table we are processing is present in repackedRels->hashtable and + * therefore, regarding logical decoding, treated like a catalog. */ ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c index a8d2e024d34..4909432d585 100644 --- a/src/backend/replication/logical/logical.c +++ b/src/backend/replication/logical/logical.c @@ -30,6 +30,7 @@ #include "access/xact.h" #include "access/xlogutils.h" +#include "commands/cluster.h" #include "fmgr.h" #include "miscadmin.h" #include "pgstat.h" @@ -112,10 +113,12 @@ CheckLogicalDecodingRequirements(void) /* * NB: Adding a new requirement likely means that RestoreSlotFromDisk() - * needs the same check. + * needs the same check. (Except that only temporary slots should be + * created for REPACK CONCURRENTLY, which effectively raises wal_level to + * LOGICAL.) */ - - if (wal_level < WAL_LEVEL_LOGICAL) + if ((wal_level < WAL_LEVEL_LOGICAL && !is_concurrent_repack_run_by_me()) + || wal_level < WAL_LEVEL_REPLICA) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("logical decoding requires \"wal_level\" >= \"logical\""))); diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c index 5acb4508f85..413bcc1addb 100644 --- a/src/backend/storage/ipc/standby.c +++ b/src/backend/storage/ipc/standby.c @@ -1313,13 +1313,13 @@ LogStandbySnapshot(void) * record. Fortunately this routine isn't executed frequently, and it's * only a shared lock. */ - if (wal_level < WAL_LEVEL_LOGICAL) + if (!XLogLogicalInfoActive()) LWLockRelease(ProcArrayLock); recptr = LogCurrentRunningXacts(running); /* Release lock if we kept it longer ... */ - if (wal_level >= WAL_LEVEL_LOGICAL) + if (XLogLogicalInfoActive()) LWLockRelease(ProcArrayLock); /* GetRunningTransactionData() acquired XidGenLock, we must release it */ diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h index d313099c027..a325bb1d16b 100644 --- a/src/include/access/xlog.h +++ b/src/include/access/xlog.h @@ -95,6 +95,12 @@ typedef enum RecoveryState extern PGDLLIMPORT int wal_level; +/* + * wal_level_transient overrides wal_level if logical decoding needs to be + * enabled transiently. + */ +extern PGDLLIMPORT int wal_level_transient; + /* Is WAL archiving enabled (always or only while server is running normally)? */ #define XLogArchivingActive() \ (AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode > ARCHIVE_MODE_OFF) @@ -122,8 +128,13 @@ extern PGDLLIMPORT int wal_level; /* Do we need to WAL-log information required only for Hot Standby and logical replication? */ #define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA) -/* Do we need to WAL-log information required only for logical replication? */ -#define XLogLogicalInfoActive() (wal_level >= WAL_LEVEL_LOGICAL) +/* + * Do we need to WAL-log information required only for logical replication? + * + * wal_level_transient overrides wal_level if logical decoding needs to be + * active transiently. + */ +#define XLogLogicalInfoActive() (Max(wal_level, wal_level_transient) == WAL_LEVEL_LOGICAL) #ifdef WAL_DEBUG extern PGDLLIMPORT bool XLOG_DEBUG; diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h index be283c70fce..0267357a261 100644 --- a/src/include/commands/cluster.h +++ b/src/include/commands/cluster.h @@ -172,6 +172,7 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, extern Size RepackShmemSize(void); extern void RepackShmemInit(void); extern bool is_concurrent_repack_in_progress(Oid relid); +extern bool is_concurrent_repack_run_by_me(void); extern void repack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel); #endif /* CLUSTER_H */ diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 372065fc570..fcbad5c1720 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -710,12 +710,16 @@ RelationCloseSmgr(Relation relation) * it would complicate decoding slightly for little gain). Note that we *do* * log information for user defined catalog tables since they presumably are * interesting to the user... + * + * If particular relations require that, the logical decoding can be active + * even if wal_level is REPLICA. Do not log other relations in that case. */ #define RelationIsLogicallyLogged(relation) \ (XLogLogicalInfoActive() && \ RelationNeedsWAL(relation) && \ (relation)->rd_rel->relkind != RELKIND_FOREIGN_TABLE && \ - !IsCatalogRelation(relation)) + !IsCatalogRelation(relation) && \ + (wal_level == WAL_LEVEL_LOGICAL || (relation)->rd_repack_concurrent)) /* routines in utils/cache/relcache.c */ extern void RelationIncrementReferenceCount(Relation rel); diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index 405d0811b4f..4f6c0ca3a8a 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -15,7 +15,6 @@ REGRESS = injection_points hashagg reindex_conc REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress ISOLATION = basic inplace syscache-update-pruned repack -ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf TAP_TESTS = 1 diff --git a/src/test/modules/injection_points/logical.conf b/src/test/modules/injection_points/logical.conf deleted file mode 100644 index c8f264bc6cb..00000000000 --- a/src/test/modules/injection_points/logical.conf +++ /dev/null @@ -1 +0,0 @@ -wal_level = logical \ No newline at end of file diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index 0e3c47ba999..716e5619aa7 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -50,9 +50,6 @@ tests += { 'syscache-update-pruned', ], 'runningcheck': false, # see syscache-update-pruned - # 'repack' requires wal_level = 'logical'. - 'regress_args': ['--temp-config', files('logical.conf')], - }, 'tap': { 'env': { diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e1e3e619c4b..b3be8572132 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2511,6 +2511,7 @@ ReorderBufferUpdateProgressTxnCB ReorderTuple RepOriginId RepackedRel +RepackedRels RepackDecodingState RepackStmt ReparameterizeForeignPathByChild_function -- 2.43.5 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v12-0009-Call-logical_rewrite_heap_tuple-when-applying-concur.patch From 30837045beccac12ed48d2b60ed3fef35367fb3c Mon Sep 17 00:00:00 2001 From: Antonin Houska Date: Tue, 1 Apr 2025 13:48:57 +0200 Subject: [PATCH 9/9] Call logical_rewrite_heap_tuple() when applying concurrent data changes. This was implemented for the sake of completeness, but I think it's currently not needed. Possible use cases could be: 1. REPACK CONCURRENTLY can process system catalogs. System catalogs are scanned using a historic snapshot during logical decoding, and the "combo CIDs" information is needed for that. Since "combo CID" is associated with the "file locator" and that locator is changed by REPACK, this command must record the information on individual tuples being moved from the old file to the new one. This is what logical_rewrite_heap_tuple() does. However, the logical decoding subsystem currently does not support decoding of data changes in the system catalog. Therefore, the CONCURRENTLY option cannot be used for system catalogs. 2. REPACK CONCURRENTLY is processing a relation, but once it has released all the locks (in order to get the exclusive lock), another backend runs REPACK CONCURRENTLY on the same table. Since the relation is treated as a system catalog while these commands are processing it (so it can be scanned using a historic snapshot during the "initial load"), it is important that the 2nd backend does not break decoding of the "combo CIDs" performed by the 1st backend. However, it's not practical to let multiple backends run REPACK CONCURRENTLY on the same relation, so we forbid that. --- src/backend/access/heap/heapam_handler.c | 2 +- src/backend/access/heap/rewriteheap.c | 65 +++++----- src/backend/commands/cluster.c | 113 +++++++++++++++--- src/backend/replication/logical/decode.c | 42 ++++++- .../pgoutput_repack/pgoutput_repack.c | 21 ++-- src/include/access/rewriteheap.h | 5 +- src/include/commands/cluster.h | 3 + src/include/replication/reorderbuffer.h | 7 ++ 8 files changed, 198 insertions(+), 60 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 850708c7830..d7b0edc3bf8 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -734,7 +734,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, /* Initialize the rewrite operation */ rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin, *xid_cutoff, - *multi_cutoff); + *multi_cutoff, true); /* Set up sorting if wanted */ diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c index 6aa2ed214f2..83076b582d7 100644 --- a/src/backend/access/heap/rewriteheap.c +++ b/src/backend/access/heap/rewriteheap.c @@ -214,10 +214,8 @@ static void raw_heap_insert(RewriteState state, HeapTuple tup); /* internal logical remapping prototypes */ static void logical_begin_heap_rewrite(RewriteState state); -static void logical_rewrite_heap_tuple(RewriteState state, ItemPointerData old_tid, HeapTuple new_tuple); static void logical_end_heap_rewrite(RewriteState state); - /* * Begin a rewrite of a table * @@ -226,18 +224,19 @@ static void logical_end_heap_rewrite(RewriteState state); * oldest_xmin xid used by the caller to determine which tuples are dead * freeze_xid xid before which tuples will be frozen * cutoff_multi multixact before which multis will be removed + * tid_chains need to maintain TID chains? * * Returns an opaque RewriteState, allocated in current memory context, * to be used in subsequent calls to the other functions. */ RewriteState begin_heap_rewrite(Relation old_heap, Relation new_heap, TransactionId oldest_xmin, - TransactionId freeze_xid, MultiXactId cutoff_multi) + TransactionId freeze_xid, MultiXactId cutoff_multi, + bool tid_chains) { RewriteState state; MemoryContext rw_cxt; MemoryContext old_cxt; - HASHCTL hash_ctl; /* * To ease cleanup, make a separate context that will contain the @@ -262,29 +261,34 @@ begin_heap_rewrite(Relation old_heap, Relation new_heap, TransactionId oldest_xm state->rs_cxt = rw_cxt; state->rs_bulkstate = smgr_bulk_start_rel(new_heap, MAIN_FORKNUM); - /* Initialize hash tables used to track update chains */ - hash_ctl.keysize = sizeof(TidHashKey); - hash_ctl.entrysize = sizeof(UnresolvedTupData); - hash_ctl.hcxt = state->rs_cxt; - - state->rs_unresolved_tups = - hash_create("Rewrite / Unresolved ctids", - 128, /* arbitrary initial size */ - &hash_ctl, - HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); - - hash_ctl.entrysize = sizeof(OldToNewMappingData); + if (tid_chains) + { + HASHCTL hash_ctl; + + /* Initialize hash tables used to track update chains */ + hash_ctl.keysize = sizeof(TidHashKey); + hash_ctl.entrysize = sizeof(UnresolvedTupData); + hash_ctl.hcxt = state->rs_cxt; + + state->rs_unresolved_tups = + hash_create("Rewrite / Unresolved ctids", + 128, /* arbitrary initial size */ + &hash_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + hash_ctl.entrysize = sizeof(OldToNewMappingData); + + state->rs_old_new_tid_map = + hash_create("Rewrite / Old to new tid map", + 128, /* arbitrary initial size */ + &hash_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + } - state->rs_old_new_tid_map = - hash_create("Rewrite / Old to new tid map", - 128, /* arbitrary initial size */ - &hash_ctl, - HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + logical_begin_heap_rewrite(state); MemoryContextSwitchTo(old_cxt); - logical_begin_heap_rewrite(state); - return state; } @@ -303,12 +307,15 @@ end_heap_rewrite(RewriteState state) * Write any remaining tuples in the UnresolvedTups table. If we have any * left, they should in fact be dead, but let's err on the safe side. */ - hash_seq_init(&seq_status, state->rs_unresolved_tups); - - while ((unresolved = hash_seq_search(&seq_status)) != NULL) + if (state->rs_unresolved_tups) { - ItemPointerSetInvalid(&unresolved->tuple->t_data->t_ctid); - raw_heap_insert(state, unresolved->tuple); + hash_seq_init(&seq_status, state->rs_unresolved_tups); + + while ((unresolved = hash_seq_search(&seq_status)) != NULL) + { + ItemPointerSetInvalid(&unresolved->tuple->t_data->t_ctid); + raw_heap_insert(state, unresolved->tuple); + } } /* Write the last page, if any */ @@ -995,7 +1002,7 @@ logical_rewrite_log_mapping(RewriteState state, TransactionId xid, * Perform logical remapping for a tuple that's mapped from old_tid to * new_tuple->t_self by rewrite_heap_tuple() if necessary for the tuple. */ -static void +void logical_rewrite_heap_tuple(RewriteState state, ItemPointerData old_tid, HeapTuple new_tuple) { diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index c2201b046bc..8472f6064ae 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -23,6 +23,7 @@ #include "access/heapam.h" #include "access/multixact.h" #include "access/relscan.h" +#include "access/rewriteheap.h" #include "access/tableam.h" #include "access/toast_internals.h" #include "access/transam.h" @@ -161,17 +162,21 @@ static HeapTuple get_changed_tuple(char *change); static void apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, ScanKey key, int nkeys, IndexInsertState *iistate, - struct timeval *must_complete); + struct timeval *must_complete, + RewriteState rwstate); static void apply_concurrent_insert(Relation rel, ConcurrentChange *change, HeapTuple tup, IndexInsertState *iistate, - TupleTableSlot *index_slot); + TupleTableSlot *index_slot, + RewriteState rwstate); static void apply_concurrent_update(Relation rel, HeapTuple tup, HeapTuple tup_target, ConcurrentChange *change, IndexInsertState *iistate, - TupleTableSlot *index_slot); + TupleTableSlot *index_slot, + RewriteState rwstate); static void apply_concurrent_delete(Relation rel, HeapTuple tup_target, - ConcurrentChange *change); + ConcurrentChange *change, + RewriteState rwstate); static HeapTuple find_target_tuple(Relation rel, ScanKey key, int nkeys, HeapTuple tup_key, Snapshot snapshot, @@ -185,7 +190,8 @@ static bool process_concurrent_changes(LogicalDecodingContext *ctx, ScanKey ident_key, int ident_key_nentries, IndexInsertState *iistate, - struct timeval *must_complete); + struct timeval *must_complete, + RewriteState rwstate); static bool processing_time_elapsed(struct timeval *must_complete); static IndexInsertState *get_index_insert_state(Relation relation, Oid ident_index_id); @@ -2747,7 +2753,7 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx, static void apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, ScanKey key, int nkeys, IndexInsertState *iistate, - struct timeval *must_complete) + struct timeval *must_complete, RewriteState rwstate) { TupleTableSlot *index_slot, *ident_slot; @@ -2822,7 +2828,8 @@ apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, { Assert(tup_old == NULL); - apply_concurrent_insert(rel, &change, tup, iistate, index_slot); + apply_concurrent_insert(rel, &change, tup, iistate, index_slot, + rwstate); pfree(tup); } @@ -2830,7 +2837,8 @@ apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, change.kind == CHANGE_DELETE) { IndexScanDesc ind_scan = NULL; - HeapTuple tup_key; + HeapTuple tup_key, + tup_exist_cp; if (change.kind == CHANGE_UPDATE_NEW) { @@ -2872,11 +2880,23 @@ apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, if (tup_exist == NULL) elog(ERROR, "Failed to find target tuple"); + /* + * Update the mapping for xmax of the old version. + * + * Use a copy ('tup_exist' can point to shared buffer) with xmin + * invalid because mapping of that should have been written on + * insertion. + */ + tup_exist_cp = heap_copytuple(tup_exist); + HeapTupleHeaderSetXmin(tup_exist_cp->t_data, InvalidTransactionId); + logical_rewrite_heap_tuple(rwstate, change.old_tid, tup_exist_cp); + pfree(tup_exist_cp); + if (change.kind == CHANGE_UPDATE_NEW) apply_concurrent_update(rel, tup, tup_exist, &change, iistate, - index_slot); + index_slot, rwstate); else - apply_concurrent_delete(rel, tup_exist, &change); + apply_concurrent_delete(rel, tup_exist, &change, rwstate); ResetRepackCurrentXids(); @@ -2929,9 +2949,12 @@ apply_concurrent_changes(RepackDecodingState *dstate, Relation rel, static void apply_concurrent_insert(Relation rel, ConcurrentChange *change, HeapTuple tup, - IndexInsertState *iistate, TupleTableSlot *index_slot) + IndexInsertState *iistate, TupleTableSlot *index_slot, + RewriteState rwstate) { + HeapTupleHeader tup_hdr = tup->t_data; Snapshot snapshot = change->snapshot; + ItemPointerData old_tid; List *recheck; /* @@ -2941,6 +2964,9 @@ apply_concurrent_insert(Relation rel, ConcurrentChange *change, HeapTuple tup, */ SetRepackCurrentXids(snapshot->subxip, snapshot->subxcnt); + /* Remember location in the old heap. */ + ItemPointerCopy(&tup_hdr->t_ctid, &old_tid); + /* * Write the tuple into the new heap. * @@ -2956,6 +2982,14 @@ apply_concurrent_insert(Relation rel, ConcurrentChange *change, HeapTuple tup, heap_insert(rel, tup, change->xid, snapshot->curcid - 1, HEAP_INSERT_NO_LOGICAL, NULL); + /* + * Update the mapping for xmin. (xmax should be invalid). This is needed + * because, during the processing, the table is considered an "user + * catalog". + */ + Assert(!TransactionIdIsValid(HeapTupleHeaderGetRawXmax(tup->t_data))); + logical_rewrite_heap_tuple(rwstate, old_tid, tup); + /* * Update indexes. * @@ -2989,15 +3023,23 @@ apply_concurrent_insert(Relation rel, ConcurrentChange *change, HeapTuple tup, static void apply_concurrent_update(Relation rel, HeapTuple tup, HeapTuple tup_target, ConcurrentChange *change, IndexInsertState *iistate, - TupleTableSlot *index_slot) + TupleTableSlot *index_slot, RewriteState rwstate) { List *recheck; LockTupleMode lockmode; TU_UpdateIndexes update_indexes; + ItemPointerData tid_new_old_heap, + tid_old_new_heap; TM_Result res; Snapshot snapshot = change->snapshot; TM_FailureData tmfd; + /* Location of the new tuple in the old heap. */ + ItemPointerCopy(&tup->t_data->t_ctid, &tid_new_old_heap); + + /* Location of the existing tuple in the new heap. */ + ItemPointerCopy(&tup_target->t_self, &tid_old_new_heap); + /* * Write the new tuple into the new heap. ('tup' gets the TID assigned * here.) @@ -3007,7 +3049,7 @@ apply_concurrent_update(Relation rel, HeapTuple tup, HeapTuple tup_target, Assert(snapshot->curcid != InvalidCommandId && snapshot->curcid > FirstCommandId); - res = heap_update(rel, &tup_target->t_self, tup, + res = heap_update(rel, &tid_old_new_heap, tup, change->xid, snapshot->curcid - 1, InvalidSnapshot, false, /* no wait - only we are doing changes */ @@ -3017,6 +3059,10 @@ apply_concurrent_update(Relation rel, HeapTuple tup, HeapTuple tup_target, if (res != TM_Ok) ereport(ERROR, (errmsg("failed to apply concurrent UPDATE"))); + /* Update the mapping for xmin of the new version. */ + Assert(!TransactionIdIsValid(HeapTupleHeaderGetRawXmax(tup->t_data))); + logical_rewrite_heap_tuple(rwstate, tid_new_old_heap, tup); + ExecStoreHeapTuple(tup, index_slot, false); if (update_indexes != TU_None) @@ -3040,8 +3086,9 @@ apply_concurrent_update(Relation rel, HeapTuple tup, HeapTuple tup_target, static void apply_concurrent_delete(Relation rel, HeapTuple tup_target, - ConcurrentChange *change) + ConcurrentChange *change, RewriteState rwstate) { + ItemPointerData tid_old_new_heap; TM_Result res; TM_FailureData tmfd; Snapshot snapshot = change->snapshot; @@ -3050,7 +3097,10 @@ apply_concurrent_delete(Relation rel, HeapTuple tup_target, Assert(snapshot->curcid != InvalidCommandId && snapshot->curcid > FirstCommandId); - res = heap_delete(rel, &tup_target->t_self, change->xid, + /* Location of the existing tuple in the new heap. */ + ItemPointerCopy(&tup_target->t_self, &tid_old_new_heap); + + res = heap_delete(rel, &tid_old_new_heap, change->xid, snapshot->curcid - 1, InvalidSnapshot, false, &tmfd, false, /* wal_logical */ @@ -3132,7 +3182,8 @@ static bool process_concurrent_changes(LogicalDecodingContext *ctx, XLogRecPtr end_of_wal, Relation rel_dst, Relation rel_src, ScanKey ident_key, int ident_key_nentries, IndexInsertState *iistate, - struct timeval *must_complete) + struct timeval *must_complete, + RewriteState rwstate) { RepackDecodingState *dstate; @@ -3165,7 +3216,8 @@ process_concurrent_changes(LogicalDecodingContext *ctx, XLogRecPtr end_of_wal, rel_dst->rd_toastoid = rel_src->rd_rel->reltoastrelid; apply_concurrent_changes(dstate, rel_dst, ident_key, - ident_key_nentries, iistate, must_complete); + ident_key_nentries, iistate, must_complete, + rwstate); } PG_FINALLY(); { @@ -3350,6 +3402,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, Oid ident_idx_old, ident_idx_new; IndexInsertState *iistate; + RewriteState rwstate; ScanKey ident_key; int ident_key_nentries; XLogRecPtr wal_insert_ptr, @@ -3437,11 +3490,27 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, * 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.) + * + * Now we are processing individual tuples, so pass false for + * 'tid_chains'. Since rwstate is now only needed for + * logical_begin_heap_rewrite(), none of the transaction IDs needs to be + * valid. */ + rwstate = begin_heap_rewrite(OldHeap, NewHeap, + InvalidTransactionId, + InvalidTransactionId, + InvalidTransactionId, + false); process_concurrent_changes(ctx, end_of_wal, NewHeap, swap_toast_by_content ? OldHeap : NULL, ident_key, ident_key_nentries, iistate, - NULL); + NULL, rwstate); + + /* + * OldHeap will be closed, so we need to initialize rwstate again for the + * next call of process_concurrent_changes(). + */ + end_heap_rewrite(rwstate); /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there @@ -3529,6 +3598,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, end_of_wal = GetFlushRecPtr(NULL); /* Apply the concurrent changes again. */ + rwstate = begin_heap_rewrite(OldHeap, NewHeap, + InvalidTransactionId, + InvalidTransactionId, + InvalidTransactionId, + false); /* * This time we have the exclusive lock on the table, so make sure that @@ -3558,11 +3632,12 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, if (!process_concurrent_changes(ctx, end_of_wal, NewHeap, swap_toast_by_content ? OldHeap : NULL, ident_key, ident_key_nentries, iistate, - t_end_ptr)) + t_end_ptr, rwstate)) ereport(ERROR, (errmsg("could not process concurrent data changes in time"), errhint("Please consider adjusting \"repack_max_xlock_time\"."))); + end_heap_rewrite(rwstate); /* Remember info about rel before closing OldHeap */ relpersistence = OldHeap->rd_rel->relpersistence; diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c index 25bb92b33f2..6f4a5f5b95b 100644 --- a/src/backend/replication/logical/decode.c +++ b/src/backend/replication/logical/decode.c @@ -984,11 +984,13 @@ DecodeInsert(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) xl_heap_insert *xlrec; ReorderBufferChange *change; RelFileLocator target_locator; + BlockNumber blknum; + HeapTupleHeader tuphdr; xlrec = (xl_heap_insert *) XLogRecGetData(r); /* only interested in our database */ - XLogRecGetBlockTag(r, 0, &target_locator, NULL, NULL); + XLogRecGetBlockTag(r, 0, &target_locator, NULL, &blknum); if (target_locator.dbOid != ctx->slot->data.database) return; @@ -1013,6 +1015,13 @@ DecodeInsert(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) DecodeXLogTuple(tupledata, datalen, change->data.tp.newtuple); + /* + * CTID is needed for logical_rewrite_heap_tuple(), when doing REPACK + * CONCURRENTLY. + */ + tuphdr = change->data.tp.newtuple->t_data; + ItemPointerSet(&tuphdr->t_ctid, blknum, xlrec->offnum); + change->data.tp.clear_toast_afterwards = true; ReorderBufferQueueChange(ctx->reorder, XLogRecGetXid(r), buf->origptr, @@ -1034,11 +1043,15 @@ DecodeUpdate(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) ReorderBufferChange *change; char *data; RelFileLocator target_locator; + BlockNumber old_blknum, + new_blknum; xlrec = (xl_heap_update *) XLogRecGetData(r); + /* Retrieve blknum, so that we can compose CTID below. */ + XLogRecGetBlockTag(r, 0, &target_locator, NULL, &new_blknum); + /* only interested in our database */ - XLogRecGetBlockTag(r, 0, &target_locator, NULL, NULL); if (target_locator.dbOid != ctx->slot->data.database) return; @@ -1055,6 +1068,7 @@ DecodeUpdate(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) { Size datalen; Size tuplelen; + HeapTupleHeader tuphdr; data = XLogRecGetBlockData(r, 0, &datalen); @@ -1064,6 +1078,13 @@ DecodeUpdate(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) ReorderBufferAllocTupleBuf(ctx->reorder, tuplelen); DecodeXLogTuple(data, datalen, change->data.tp.newtuple); + + /* + * CTID is needed for logical_rewrite_heap_tuple(), when doing REPACK + * CONCURRENTLY. + */ + tuphdr = change->data.tp.newtuple->t_data; + ItemPointerSet(&tuphdr->t_ctid, new_blknum, xlrec->new_offnum); } if (xlrec->flags & XLH_UPDATE_CONTAINS_OLD) @@ -1082,6 +1103,14 @@ DecodeUpdate(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) DecodeXLogTuple(data, datalen, change->data.tp.oldtuple); } + /* + * Remember the old tuple CTID, for the sake of + * logical_rewrite_heap_tuple(). + */ + if (!XLogRecGetBlockTagExtended(r, 1, NULL, NULL, &old_blknum, NULL)) + old_blknum = new_blknum; + ItemPointerSet(&change->data.tp.old_tid, old_blknum, xlrec->old_offnum); + change->data.tp.clear_toast_afterwards = true; ReorderBufferQueueChange(ctx->reorder, XLogRecGetXid(r), buf->origptr, @@ -1100,11 +1129,12 @@ DecodeDelete(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) xl_heap_delete *xlrec; ReorderBufferChange *change; RelFileLocator target_locator; + BlockNumber blknum; xlrec = (xl_heap_delete *) XLogRecGetData(r); /* only interested in our database */ - XLogRecGetBlockTag(r, 0, &target_locator, NULL, NULL); + XLogRecGetBlockTag(r, 0, &target_locator, NULL, &blknum); if (target_locator.dbOid != ctx->slot->data.database) return; @@ -1136,6 +1166,12 @@ DecodeDelete(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) DecodeXLogTuple((char *) xlrec + SizeOfHeapDelete, datalen, change->data.tp.oldtuple); + + /* + * CTID is needed for logical_rewrite_heap_tuple(), when doing REPACK + * CONCURRENTLY. + */ + ItemPointerSet(&change->data.tp.old_tid, blknum, xlrec->offnum); } change->data.tp.clear_toast_afterwards = true; diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c index 28bd16f9cc7..24d9c9c4884 100644 --- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c +++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c @@ -33,7 +33,7 @@ static void plugin_truncate(struct LogicalDecodingContext *ctx, ReorderBufferChange *change); static void store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind, HeapTuple tuple, - TransactionId xid); + TransactionId xid, ItemPointer old_tid); void _PG_output_plugin_init(OutputPluginCallbacks *cb) @@ -168,7 +168,8 @@ plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (newtuple == NULL) elog(ERROR, "Incomplete insert info."); - store_change(ctx, CHANGE_INSERT, newtuple, change->txn->xid); + store_change(ctx, CHANGE_INSERT, newtuple, change->txn->xid, + NULL); } break; case REORDER_BUFFER_CHANGE_UPDATE: @@ -186,10 +187,10 @@ plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (oldtuple != NULL) store_change(ctx, CHANGE_UPDATE_OLD, oldtuple, - change->txn->xid); + change->txn->xid, NULL); store_change(ctx, CHANGE_UPDATE_NEW, newtuple, - change->txn->xid); + change->txn->xid, &change->data.tp.old_tid); } break; case REORDER_BUFFER_CHANGE_DELETE: @@ -202,7 +203,8 @@ plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (oldtuple == NULL) elog(ERROR, "Incomplete delete info."); - store_change(ctx, CHANGE_DELETE, oldtuple, change->txn->xid); + store_change(ctx, CHANGE_DELETE, oldtuple, change->txn->xid, + &change->data.tp.old_tid); } break; default: @@ -236,13 +238,13 @@ plugin_truncate(struct LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (i == nrelations) return; - store_change(ctx, CHANGE_TRUNCATE, NULL, InvalidTransactionId); + store_change(ctx, CHANGE_TRUNCATE, NULL, InvalidTransactionId, NULL); } /* Store concurrent data change. */ static void store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind, - HeapTuple tuple, TransactionId xid) + HeapTuple tuple, TransactionId xid, ItemPointer old_tid) { RepackDecodingState *dstate; char *change_raw; @@ -317,6 +319,11 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind, change.snapshot = dstate->snapshot; dstate->snapshot->active_count++; + if (old_tid) + ItemPointerCopy(old_tid, &change.old_tid); + else + ItemPointerSetInvalid(&change.old_tid); + /* The data has been copied. */ if (flattened) pfree(tuple); diff --git a/src/include/access/rewriteheap.h b/src/include/access/rewriteheap.h index 99c3f362adc..eebda35c7cb 100644 --- a/src/include/access/rewriteheap.h +++ b/src/include/access/rewriteheap.h @@ -23,11 +23,14 @@ typedef struct RewriteStateData *RewriteState; extern RewriteState begin_heap_rewrite(Relation old_heap, Relation new_heap, TransactionId oldest_xmin, TransactionId freeze_xid, - MultiXactId cutoff_multi); + MultiXactId cutoff_multi, bool tid_chains); extern void end_heap_rewrite(RewriteState state); extern void rewrite_heap_tuple(RewriteState state, HeapTuple old_tuple, HeapTuple new_tuple); extern bool rewrite_heap_dead_tuple(RewriteState state, HeapTuple old_tuple); +extern void logical_rewrite_heap_tuple(RewriteState state, + ItemPointerData old_tid, + HeapTuple new_tuple); /* * On-Disk data format for an individual logical rewrite mapping. diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h index 0267357a261..45cd3fe4276 100644 --- a/src/include/commands/cluster.h +++ b/src/include/commands/cluster.h @@ -78,6 +78,9 @@ typedef struct ConcurrentChange /* Transaction that changes the data. */ TransactionId xid; + /* For UPDATE / DELETE, the location of the old tuple version. */ + ItemPointerData old_tid; + /* * Historic catalog snapshot that was used to decode this change. */ diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h index 3be0cbd7ebe..c2731947b22 100644 --- a/src/include/replication/reorderbuffer.h +++ b/src/include/replication/reorderbuffer.h @@ -104,6 +104,13 @@ typedef struct ReorderBufferChange HeapTuple oldtuple; /* valid for INSERT || UPDATE */ HeapTuple newtuple; + + /* + * REPACK CONCURRENTLY needs the old TID, even if the old tuple + * itself is not WAL-logged (i.e. when the identity key does not + * change). + */ + ItemPointerData old_tid; } tp; /* -- 2.43.5 --=-=-=--