public inbox for [email protected]help / color / mirror / Atom feed
[PATCH 2/2] review reworks 3+ messages / 3 participants [nested] [flat]
* [PATCH 2/2] review reworks @ 2019-07-06 14:39 Tomas Vondra <[email protected]> 0 siblings, 0 replies; 3+ messages in thread From: Tomas Vondra @ 2019-07-06 14:39 UTC (permalink / raw) --- src/backend/access/heap/tuptoaster.c | 47 +++++++++++++++------------- src/common/pg_lzcompress.c | 19 +++++------ src/include/common/pg_lzcompress.h | 2 +- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/backend/access/heap/tuptoaster.c b/src/backend/access/heap/tuptoaster.c index 1eb6ccaca2..809a52196c 100644 --- a/src/backend/access/heap/tuptoaster.c +++ b/src/backend/access/heap/tuptoaster.c @@ -62,7 +62,7 @@ typedef struct toast_compress_header #define TOAST_COMPRESS_HDRSZ ((int32) sizeof(toast_compress_header)) #define TOAST_COMPRESS_RAWSIZE(ptr) (((toast_compress_header *) (ptr))->rawsize) #define TOAST_COMPRESS_SIZE(ptr) ((int32) VARSIZE(ptr) - TOAST_COMPRESS_HDRSZ) -#define TOAST_COMPRESS_DATA(ptr) \ +#define TOAST_COMPRESS_RAWDATA(ptr) \ (((char *) (ptr)) + TOAST_COMPRESS_HDRSZ) #define TOAST_COMPRESS_SET_RAWSIZE(ptr, len) \ (((toast_compress_header *) (ptr))->rawsize = (len)) @@ -254,7 +254,7 @@ heap_tuple_untoast_attr(struct varlena *attr) * Public entry point to get back part of a toasted value * from compression or external storage. * - * Note if slicelength is negative, return suffix of the value. + * Note: when slicelength is negative, return suffix of the value. * ---------- */ struct varlena * @@ -277,17 +277,23 @@ heap_tuple_untoast_attr_slice(struct varlena *attr, return toast_fetch_datum_slice(attr, sliceoffset, slicelength); /* - * If only need the prefix slice, fetch enough datums to decompress. - * Otherwise, fetch all datums. + * When a prefix of the value is requested, fetch enough slices to + * decompress. Otherwise, fetch all slices. */ if (slicelength > 0 && sliceoffset >= 0) { int32 max_size; + + /* + * Determine maximum amount of compressed data needed for a prefix + * of a given length (after decompression). + */ max_size = pglz_maximum_compressed_size(sliceoffset + slicelength, TOAST_COMPRESS_SIZE(attr)); + /* - * Be sure to get enough compressed slice - * and compressed marker will get set automatically + * Fetch enough compressed slices, compressed marker will get set + * automatically. */ preslice = toast_fetch_datum_slice(attr, 0, max_size); } @@ -1409,7 +1415,7 @@ toast_compress_datum(Datum value) */ len = pglz_compress(VARDATA_ANY(DatumGetPointer(value)), valsize, - TOAST_COMPRESS_DATA(tmp), + TOAST_COMPRESS_RAWDATA(tmp), PGLZ_strategy_default); if (len >= 0 && len + TOAST_COMPRESS_HDRSZ < valsize - 2) @@ -2050,7 +2056,8 @@ toast_fetch_datum(struct varlena *attr) * in the toast relation * * Note that this function supports non-compressed external datums - * and compressed external datum slices at the start of the object. + * and compressed external datums (in which case the requrested slice + * has to be a prefix, i.e. sliceoffset has to be 0). * ---------- */ static struct varlena * @@ -2092,7 +2099,8 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset, int32 length) /* * It's meaningful to fetch slices at the start of a compressed datum, - * since we can decompress the prefix raw data from it. + * since we can decompress the prefix raw data without decompressing the + * whole value. */ Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset); @@ -2105,26 +2113,23 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset, int32 length) length = 0; } + /* + * When fetching a prefix of a compressed external datum, account for + * the rawsize tracking amount of raw data (it's stored at the beginning + * as an int32 value). + */ if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && length > 0) - { - /* - * If we are going to fetch the compressed external datum slice - * at the start of the object, also should fetch rawsize field used - * to record the size of the raw data. - */ length = length + sizeof(int32); - } if (((sliceoffset + length) > attrsize) || length < 0) length = attrsize - sliceoffset; result = (struct varlena *) palloc(length + VARHDRSZ); - if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer)) { + if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer)) SET_VARSIZE_COMPRESSED(result, length + VARHDRSZ); - } else { + else SET_VARSIZE(result, length + VARHDRSZ); - } if (length == 0) return result; /* Can save a lot of work at this point! */ @@ -2307,7 +2312,7 @@ toast_decompress_datum(struct varlena *attr) palloc(TOAST_COMPRESS_RAWSIZE(attr) + VARHDRSZ); SET_VARSIZE(result, TOAST_COMPRESS_RAWSIZE(attr) + VARHDRSZ); - if (pglz_decompress(TOAST_COMPRESS_DATA(attr), + if (pglz_decompress(TOAST_COMPRESS_RAWDATA(attr), TOAST_COMPRESS_SIZE(attr), VARDATA(result), TOAST_COMPRESS_RAWSIZE(attr), true) < 0) @@ -2334,7 +2339,7 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength) result = (struct varlena *) palloc(slicelength + VARHDRSZ); - rawsize = pglz_decompress(TOAST_COMPRESS_DATA(attr), + rawsize = pglz_decompress(TOAST_COMPRESS_RAWDATA(attr), VARSIZE(attr) - TOAST_COMPRESS_HDRSZ, VARDATA(result), slicelength, false); diff --git a/src/common/pg_lzcompress.c b/src/common/pg_lzcompress.c index ac7d66db77..18766ade02 100644 --- a/src/common/pg_lzcompress.c +++ b/src/common/pg_lzcompress.c @@ -773,27 +773,28 @@ pglz_decompress(const char *source, int32 slen, char *dest, } - /* ---------- * pglz_max_compressed_size - * - * Calculate the maximum size of the compressed slice corresponding to the - * raw slice. Return the maximum size, or total compressed size if maximum - * size is larger than total compressed size. + * Calculate the maximum compressed size for a given amount of raw data. + * Return the maximum size, or total compressed size if maximum size is + * larger than total compressed size. * ---------- */ int32 -pglz_maximum_compressed_size(int32 raw_slice_size, int32 total_compressed_size) +pglz_maximum_compressed_size(int32 rawsize, int32 total_compressed_size) { - int32 result; + int32 compressed_size; /* * Use int64 to prevent overflow during calculation. */ - result = (int32)((int64)raw_slice_size * 9 + 8) / 8; + compressed_size = (int32) ((int64) rawsize * 9 + 8) / 8; /* - * Note that slice compressed size will never be larger than total compressed size. + * Maximum compressed size can't be larger than total compressed size. */ - return result > total_compressed_size ? total_compressed_size: result; + compressed_size = Min(compressed_size, total_compressed_size); + + return compressed_size; } diff --git a/src/include/common/pg_lzcompress.h b/src/include/common/pg_lzcompress.h index cda3e1d364..e59fe2eaea 100644 --- a/src/include/common/pg_lzcompress.h +++ b/src/include/common/pg_lzcompress.h @@ -87,6 +87,6 @@ extern int32 pglz_compress(const char *source, int32 slen, char *dest, const PGLZ_Strategy *strategy); extern int32 pglz_decompress(const char *source, int32 slen, char *dest, int32 rawsize, bool check_complete); -extern int32 pglz_maximum_compressed_size(int32 raw_slice_size, int32 raw_size); +extern int32 pglz_maximum_compressed_size(int32 rawsize, int32 total_compressed_size); #endif /* _PG_LZCOMPRESS_H_ */ -- 2.20.1 --3wvarhewrzuvoyhd Content-Type: application/x-sh Content-Disposition: attachment; filename="random-bench2.sh" Content-Transfer-Encoding: quoted-printable #!/bin/bash=0A=0ALEN=3D100000=0AROWS=3D1000=0A=0Afor a in 0 100 1000 10000 = 100000; do=0A=0A b=3D$((LEN - a))=0A=0A dropdb --if-exists test > /dev/null= 2>&1=0A createdb test > /dev/null 2>&1=0A=0A psql test -c "create table t = (a text)" > /dev/null 2>&1=0A=0A psql test -c "create extension randomstrin= g" > /dev/null 2>&1=0A=0A psql test -c "insert into t select random_string(= $a) || repeat('a', $b) from generate_series(1,$ROWS) S(i)" > /dev/null 2>&1= =0A=0A psql test -c "vacuum analyze t" > /dev/null 2>&1=0A=0A psql test -c = "checkpoint" > /dev/null 2>&1=0A=0A for p in 0 100 1000 10000 100000; do=0A= =0A for l in 100 1000 10000 100000; do=0A=0A for r in `seq 1 10`; do=0A = psql test > tmp.log <<EOF=0A\timing on=0Aselect sum(length(substr(a,$p,$= l))) from t=0AEOF=0A=0A t=3D`cat tmp.log | grep Time | awk '{print $2}'`= =0A=0A echo $LEN $a $b $p $l $r random-first $t=0A=0A done=0A=0A done= =0A=0A done=0A=0Adone=0A=0Afor a in 0 100 1000 10000 100000; do=0A=0A b=3D$= ((LEN - a))=0A=0A dropdb --if-exists test > /dev/null 2>&1=0A createdb test= > /dev/null 2>&1=0A=0A psql test -c "create table t (a text)" > /dev/null = 2>&1=0A=0A psql test -c "create extension randomstring" > /dev/null 2>&1=0A= =0A psql test -c "insert into t select repeat('a', $b) || random_string($a)= from generate_series(1,$ROWS) S(i)" > /dev/null 2>&1=0A=0A psql test -c "v= acuum analyze t" > /dev/null 2>&1=0A=0A psql test -c "checkpoint" > /dev/nu= ll 2>&1=0A=0A for p in 0 100 1000 10000 100000; do=0A=0A for l in 100 1000= 10000 100000; do=0A=0A for r in `seq 1 10`; do=0A psql test > tmp.log= <<EOF=0A\timing on=0Aselect sum(length(substr(a,$p,$l))) from t=0AEOF=0A= =0A t=3D`cat tmp.log | grep Time | awk '{print $2}'`=0A=0A echo $LEN = $a $b $p $l $r random-last $t=0A=0A done=0A=0A done=0A=0A done=0A=0Adone= =0A --3wvarhewrzuvoyhd Content-Type: application/vnd.oasis.opendocument.spreadsheet Content-Disposition: attachment; filename="comparison.ods" Content-Transfer-Encoding: base64 UEsDBBQAAAgAAGJp5k6FbDmKLgAAAC4AAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQu b2FzaXMub3BlbmRvY3VtZW50LnNwcmVhZHNoZWV0UEsDBBQAAAgAAGJp5k7m41/j3iIAAN4i AAAYAAAAVGh1bWJuYWlscy90aHVtYm5haWwucG5niVBORw0KGgoAAAANSUhEUgAAAL4AAAEA CAMAAAA6HTweAAADAFBMVEUAAAAEDQcKAAALCwsFEAkRCwsVFRUAIAkMJBQANA8AOBESJhgd KSAUPCAuBQUlHx81AQEwGxsiIiIvMC8wKCgwMDA2PDg6MzM4ODgAShYBVRoZRicdVC4eWDAA Yx0CaiEBdyQjRC0mTDIpWTc7Qj0wXD0nZjoncj4wYD41W0Eua0Aqe0M1Z0Q3dUo+e1FLEBBZ FBRMNDRULy9bOztrAABlFhZ6AAB4GxtjNzdERERFSkdGTUhLTEtHVkxWSkpUVVVVWFZZU1Nb W1tEYExKaFNDfFRbYl1SdFxeZWBXfGJoR0dkWVl1SEh5VFRnZ2dpcmxudXB2Z2dycnJ0enV2 fHh6cnJ7e3sCiCoPjjUBli4EmzICqTUSr0IuhUkxj047iFI2l1M6p1s9r2A+smJGhVlEl11M jmBKmWJdgmhSmmhHpGNDvmhUpWxZqXFQt25YuHVhiGxljnFsmXp4gntjqnhhtHpyoH9WxXdi wX5+hYBmv4F5qod+so1rzIh2wo160pWEAACDHR2VAACNICCaIiKkAAC0AACiJSW2KSmITEyH W1uXVFSFaGiGeHiZaGiYd3eiW1uiamqlc3O1Zma0enrMAADPFxfYAADKLi7OMjLWMTHpAAD+ AAD/HR3mNDT+OzvEbm7Nc3PYeXnif3//fn6IiYiJk4yOl5CYiIiWl5aPoJWAtI+IvJeYpJyd paCcuaWhiYmjmJi7g4O5mJiioqKjqqWnramsoKCqqqqotKyttrC2p6ezs7OzurW8tLS7u7uK w5uB1JqPyKCRzKOZ2q6d3bCuwrSg3rO3xbuq4ru8y8G90sS05cPMh4fMlJTUjIzalJTIpaXK urrSra3YtLTogoLmmJj5jIz+kpLrpaXlvLz+qKj+srLAxb78wb/FxcXCysXGzMjOxcXNzc3H 1cvN2tHVycnT09PS29XW3Nja09Pb29vB6c3M6dXY5NzV8N3e7OLZ8uHmx8fp19f9ysr+0tLl 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 AAsAAABjb250ZW50LnhtbOy9W3NdR64m+D6/osInet60lPeLp6r6oU1SvN+lieiYOMGyaJei ZdFB0XX59wOstS3DMrc3AGXmplSu060qWxCV30okEvgSlz//z3/98PZP/7i9f//m7t1fvrKT +epPt+++vXv95t33f/nq+mr3Wfnqf/71//rz3Xffvfn29uvXd9/+9MPtu4dn3969e4D//hP8 6Xfvv15+9y9f/XT/7uu7m/dv3n/97uaH2/dfP3z79d2Pt+9+/lNfU+mv579r+TfvH/79lv3H Z2H6px9u//XA/cMo+6s/e/M3/t88C9M//fr+5p/cP4yy8FHpH//ujvuH//X+7bPv7uCr//Dj zcObj1bxr7dv3v2fv3z194eHH79+/vyf//zn9E8/3d1//9zWWp/Pv/thwd9+kPvxp/u3s9Tr b5/fvr3Fv+z9czvZ5z/L/nD7cMNdH8rSJb376Ye/3d6zP83Nw81vdvXH+9v3IAJwUTF5P4j+ mV/p1z++Z2vXP75f85m//fvNPVvPZuFfq4p/zVcV/5r+2R9uHv6+Zn/L82P4zfmX46Nf9Or+ B+7fhbK/+lTf3r/5kQ1zkaZ//u7u7sNS8Q8sh31erjMmPF/+mUj/83fF/3n/5uH2noh/+7vi 3968/fbDF7/74bGPBnL2OUg8u/0HqvyHQ4Qf4v2aP+CeL7/9Qfj967U/+v89Prr89u+3P9z8 Ivxms/CzN+/eP9y8++XL3OMmrEUan9/f/nh3//Dhw3zHN76wW+7D2v7+8MPb9aYDf/dn0e/v X79+VBSW45+DGYFD/Owfb27/+V+/sq2/rw/1+SxE7env/gFrnqPMhyMJ2/2L8b///sP99N3d T+9eL2Zg+Ri3//rx9v4N/tbN2/mPff2rn0A1+O2d4keu7jTyE35l6N/cvv359H+A9OiPubt7 9sN7UAZQ+rsfvyZ/+tf3xv0P/+L9OFT0u9ffffwTPzr0375/7x8e29uri+f4e8/w2oSLYfU3 EXfBffXXn32DxRa8f/7hX3wHPsKz726+vX32+vbbt+//+ufFxn/4139a/hnX/Zevjt7AhTF/ 3j9d3ryDkwaG+GfRH968/fdfvvq/b368e///fCS3/Muv/vSrH43yz76/fQcfAszF+3++ef/+ VxI/vnn4FgzqP27u38za9/z3l/bN/d2b1/Pf9qdvbv9x8+7me/iDaxf4qDRnmf9+/3D7Q6N1 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 bFBLBQYAAAAAEQARAGUEAAAYvgAAAAA= --3wvarhewrzuvoyhd-- ^ permalink raw reply [nested|flat] 3+ messages in thread
* [PATCH v8 3/4] Convert pg_restore's ready_list to a priority queue. @ 2023-07-20 17:19 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 3+ messages in thread From: Nathan Bossart @ 2023-07-20 17:19 UTC (permalink / raw) Presently, we spend a lot of time sorting this list so that we pick the largest items first. With many tables, this sorting can become a significant bottleneck. There are a couple of reports from the field about this, and it is easily reproducible, so this is not a hypothetical issue. This commit improves the performance of pg_restore with many tables by converting its ready_list to a priority queue, i.e., a binary heap. We will first try to run the highest priority item, but if it cannot be chosen due to the lock heuristic, we'll do a sequential scan through the heap nodes until we find one that is runnable. This means that we might end up picking an item with much lower priority, but since we expect that we'll typically be able to choose one of the first few nodes, we should usually pick an item with a relatively high priority. On my machine, a basic test with 100,000 tables takes 11.5 minutes without this patch and 1.5 minutes with it. Pierre Ducroquet claims to see a speedup from 30 minutes to 23 minutes for a real-world dump of over 50,000 tables. Suggested-by: Tom Lane Tested-by: Pierre Ducroquet Reviewed-by: Tom Lane Discussion: https://postgr.es/m/3612876.1689443232%40sss.pgh.pa.us --- src/bin/pg_dump/pg_backup_archiver.c | 197 ++++++++------------------- 1 file changed, 57 insertions(+), 140 deletions(-) diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 4d83381d84..95b8f69ecf 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -34,6 +34,7 @@ #include "compress_io.h" #include "dumputils.h" #include "fe_utils/string_utils.h" +#include "lib/binaryheap.h" #include "lib/stringinfo.h" #include "libpq/libpq-fs.h" #include "parallel.h" @@ -44,24 +45,6 @@ #define TEXT_DUMP_HEADER "--\n-- PostgreSQL database dump\n--\n\n" #define TEXT_DUMPALL_HEADER "--\n-- PostgreSQL database cluster dump\n--\n\n" -/* - * State for tracking TocEntrys that are ready to process during a parallel - * restore. (This used to be a list, and we still call it that, though now - * it's really an array so that we can apply qsort to it.) - * - * tes[] is sized large enough that we can't overrun it. - * The valid entries are indexed first_te .. last_te inclusive. - * We periodically sort the array to bring larger-by-dataLength entries to - * the front; "sorted" is true if the valid entries are known sorted. - */ -typedef struct _parallelReadyList -{ - TocEntry **tes; /* Ready-to-dump TocEntrys */ - int first_te; /* index of first valid entry in tes[] */ - int last_te; /* index of last valid entry in tes[] */ - bool sorted; /* are valid entries currently sorted? */ -} ParallelReadyList; - static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt, const pg_compress_specification compression_spec, @@ -111,16 +94,12 @@ static void restore_toc_entries_postfork(ArchiveHandle *AH, static void pending_list_header_init(TocEntry *l); static void pending_list_append(TocEntry *l, TocEntry *te); static void pending_list_remove(TocEntry *te); -static void ready_list_init(ParallelReadyList *ready_list, int tocCount); -static void ready_list_free(ParallelReadyList *ready_list); -static void ready_list_insert(ParallelReadyList *ready_list, TocEntry *te); -static void ready_list_remove(ParallelReadyList *ready_list, int i); -static void ready_list_sort(ParallelReadyList *ready_list); -static int TocEntrySizeCompare(const void *p1, const void *p2); -static void move_to_ready_list(TocEntry *pending_list, - ParallelReadyList *ready_list, +static int TocEntrySizeCompareQsort(const void *p1, const void *p2); +static int TocEntrySizeCompareBinaryheap(void *p1, void *p2, void *arg); +static void move_to_ready_heap(TocEntry *pending_list, + binaryheap *ready_heap, RestorePass pass); -static TocEntry *pop_next_work_item(ParallelReadyList *ready_list, +static TocEntry *pop_next_work_item(binaryheap *ready_heap, ParallelState *pstate); static void mark_dump_job_done(ArchiveHandle *AH, TocEntry *te, @@ -135,7 +114,7 @@ static bool has_lock_conflicts(TocEntry *te1, TocEntry *te2); static void repoint_table_dependencies(ArchiveHandle *AH); static void identify_locking_dependencies(ArchiveHandle *AH, TocEntry *te); static void reduce_dependencies(ArchiveHandle *AH, TocEntry *te, - ParallelReadyList *ready_list); + binaryheap *ready_heap); static void mark_create_done(ArchiveHandle *AH, TocEntry *te); static void inhibit_data_for_failed_table(ArchiveHandle *AH, TocEntry *te); @@ -2384,7 +2363,7 @@ WriteDataChunks(ArchiveHandle *AH, ParallelState *pstate) } if (ntes > 1) - qsort(tes, ntes, sizeof(TocEntry *), TocEntrySizeCompare); + qsort(tes, ntes, sizeof(TocEntry *), TocEntrySizeCompareQsort); for (int i = 0; i < ntes; i++) DispatchJobForTocEntry(AH, pstate, tes[i], ACT_DUMP, @@ -3984,7 +3963,7 @@ restore_toc_entries_prefork(ArchiveHandle *AH, TocEntry *pending_list) (void) restore_toc_entry(AH, next_work_item, false); - /* Reduce dependencies, but don't move anything to ready_list */ + /* Reduce dependencies, but don't move anything to ready_heap */ reduce_dependencies(AH, next_work_item, NULL); } else @@ -4027,24 +4006,26 @@ static void restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate, TocEntry *pending_list) { - ParallelReadyList ready_list; + binaryheap *ready_heap; TocEntry *next_work_item; pg_log_debug("entering restore_toc_entries_parallel"); - /* Set up ready_list with enough room for all known TocEntrys */ - ready_list_init(&ready_list, AH->tocCount); + /* Set up ready_heap with enough room for all known TocEntrys */ + ready_heap = binaryheap_allocate(AH->tocCount, + TocEntrySizeCompareBinaryheap, + NULL); /* * The pending_list contains all items that we need to restore. Move all - * items that are available to process immediately into the ready_list. + * items that are available to process immediately into the ready_heap. * After this setup, the pending list is everything that needs to be done - * but is blocked by one or more dependencies, while the ready list + * but is blocked by one or more dependencies, while the ready heap * contains items that have no remaining dependencies and are OK to * process in the current restore pass. */ AH->restorePass = RESTORE_PASS_MAIN; - move_to_ready_list(pending_list, &ready_list, AH->restorePass); + move_to_ready_heap(pending_list, ready_heap, AH->restorePass); /* * main parent loop @@ -4058,7 +4039,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate, for (;;) { /* Look for an item ready to be dispatched to a worker */ - next_work_item = pop_next_work_item(&ready_list, pstate); + next_work_item = pop_next_work_item(ready_heap, pstate); if (next_work_item != NULL) { /* If not to be restored, don't waste time launching a worker */ @@ -4068,7 +4049,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate, next_work_item->dumpId, next_work_item->desc, next_work_item->tag); /* Update its dependencies as though we'd completed it */ - reduce_dependencies(AH, next_work_item, &ready_list); + reduce_dependencies(AH, next_work_item, ready_heap); /* Loop around to see if anything else can be dispatched */ continue; } @@ -4079,7 +4060,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate, /* Dispatch to some worker */ DispatchJobForTocEntry(AH, pstate, next_work_item, ACT_RESTORE, - mark_restore_job_done, &ready_list); + mark_restore_job_done, ready_heap); } else if (IsEveryWorkerIdle(pstate)) { @@ -4093,7 +4074,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate, /* Advance to next restore pass */ AH->restorePass++; /* That probably allows some stuff to be made ready */ - move_to_ready_list(pending_list, &ready_list, AH->restorePass); + move_to_ready_heap(pending_list, ready_heap, AH->restorePass); /* Loop around to see if anything's now ready */ continue; } @@ -4122,10 +4103,10 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate, next_work_item ? WFW_ONE_IDLE : WFW_GOT_STATUS); } - /* There should now be nothing in ready_list. */ - Assert(ready_list.first_te > ready_list.last_te); + /* There should now be nothing in ready_heap. */ + Assert(binaryheap_empty(ready_heap)); - ready_list_free(&ready_list); + binaryheap_free(ready_heap); pg_log_info("finished main parallel loop"); } @@ -4225,80 +4206,9 @@ pending_list_remove(TocEntry *te) } -/* - * Initialize the ready_list with enough room for up to tocCount entries. - */ -static void -ready_list_init(ParallelReadyList *ready_list, int tocCount) -{ - ready_list->tes = (TocEntry **) - pg_malloc(tocCount * sizeof(TocEntry *)); - ready_list->first_te = 0; - ready_list->last_te = -1; - ready_list->sorted = false; -} - -/* - * Free storage for a ready_list. - */ -static void -ready_list_free(ParallelReadyList *ready_list) -{ - pg_free(ready_list->tes); -} - -/* Add te to the ready_list */ -static void -ready_list_insert(ParallelReadyList *ready_list, TocEntry *te) -{ - ready_list->tes[++ready_list->last_te] = te; - /* List is (probably) not sorted anymore. */ - ready_list->sorted = false; -} - -/* Remove the i'th entry in the ready_list */ -static void -ready_list_remove(ParallelReadyList *ready_list, int i) -{ - int f = ready_list->first_te; - - Assert(i >= f && i <= ready_list->last_te); - - /* - * In the typical case where the item to be removed is the first ready - * entry, we need only increment first_te to remove it. Otherwise, move - * the entries before it to compact the list. (This preserves sortedness, - * if any.) We could alternatively move the entries after i, but there - * are typically many more of those. - */ - if (i > f) - { - TocEntry **first_te_ptr = &ready_list->tes[f]; - - memmove(first_te_ptr + 1, first_te_ptr, (i - f) * sizeof(TocEntry *)); - } - ready_list->first_te++; -} - -/* Sort the ready_list into the desired order */ -static void -ready_list_sort(ParallelReadyList *ready_list) -{ - if (!ready_list->sorted) - { - int n = ready_list->last_te - ready_list->first_te + 1; - - if (n > 1) - qsort(ready_list->tes + ready_list->first_te, n, - sizeof(TocEntry *), - TocEntrySizeCompare); - ready_list->sorted = true; - } -} - /* qsort comparator for sorting TocEntries by dataLength */ static int -TocEntrySizeCompare(const void *p1, const void *p2) +TocEntrySizeCompareQsort(const void *p1, const void *p2) { const TocEntry *te1 = *(const TocEntry *const *) p1; const TocEntry *te2 = *(const TocEntry *const *) p2; @@ -4318,17 +4228,24 @@ TocEntrySizeCompare(const void *p1, const void *p2) return 0; } +/* binaryheap comparator for sorting TocEntries by dataLength */ +static int +TocEntrySizeCompareBinaryheap(void *p1, void *p2, void *arg) +{ + return TocEntrySizeCompareQsort(p1, p2); +} + /* - * Move all immediately-ready items from pending_list to ready_list. + * Move all immediately-ready items from pending_list to ready_heap. * * Items are considered ready if they have no remaining dependencies and * they belong in the current restore pass. (See also reduce_dependencies, * which applies the same logic one-at-a-time.) */ static void -move_to_ready_list(TocEntry *pending_list, - ParallelReadyList *ready_list, +move_to_ready_heap(TocEntry *pending_list, + binaryheap *ready_heap, RestorePass pass) { TocEntry *te; @@ -4344,38 +4261,38 @@ move_to_ready_list(TocEntry *pending_list, { /* Remove it from pending_list ... */ pending_list_remove(te); - /* ... and add to ready_list */ - ready_list_insert(ready_list, te); + /* ... and add to ready_heap */ + binaryheap_add(ready_heap, te); } } } /* * Find the next work item (if any) that is capable of being run now, - * and remove it from the ready_list. + * and remove it from the ready_heap. * * Returns the item, or NULL if nothing is runnable. * * To qualify, the item must have no remaining dependencies * and no requirements for locks that are incompatible with - * items currently running. Items in the ready_list are known to have + * items currently running. Items in the ready_heap are known to have * no remaining dependencies, but we have to check for lock conflicts. */ static TocEntry * -pop_next_work_item(ParallelReadyList *ready_list, +pop_next_work_item(binaryheap *ready_heap, ParallelState *pstate) { /* - * Sort the ready_list so that we'll tackle larger jobs first. - */ - ready_list_sort(ready_list); - - /* - * Search the ready_list until we find a suitable item. + * Search the ready_heap until we find a suitable item. Note that we do a + * sequential scan through the heap nodes, so even though we will first + * try to choose the highest-priority item, we might end up picking + * something with a much lower priority. However, it is expected that we + * will typically be able to pick one of the first few items, which should + * usually have a relatively high priority. */ - for (int i = ready_list->first_te; i <= ready_list->last_te; i++) + for (int i = 0; i < binaryheap_size(ready_heap); i++) { - TocEntry *te = ready_list->tes[i]; + TocEntry *te = (TocEntry *) binaryheap_get_node(ready_heap, i); bool conflicts = false; /* @@ -4401,7 +4318,7 @@ pop_next_work_item(ParallelReadyList *ready_list, continue; /* passed all tests, so this item can run */ - ready_list_remove(ready_list, i); + binaryheap_remove_node(ready_heap, i); return te; } @@ -4447,7 +4364,7 @@ mark_restore_job_done(ArchiveHandle *AH, int status, void *callback_data) { - ParallelReadyList *ready_list = (ParallelReadyList *) callback_data; + binaryheap *ready_heap = (binaryheap *) callback_data; pg_log_info("finished item %d %s %s", te->dumpId, te->desc, te->tag); @@ -4465,7 +4382,7 @@ mark_restore_job_done(ArchiveHandle *AH, pg_fatal("worker process failed: exit code %d", status); - reduce_dependencies(AH, te, ready_list); + reduce_dependencies(AH, te, ready_heap); } @@ -4708,11 +4625,11 @@ identify_locking_dependencies(ArchiveHandle *AH, TocEntry *te) /* * Remove the specified TOC entry from the depCounts of items that depend on * it, thereby possibly making them ready-to-run. Any pending item that - * becomes ready should be moved to the ready_list, if that's provided. + * becomes ready should be moved to the ready_heap, if that's provided. */ static void reduce_dependencies(ArchiveHandle *AH, TocEntry *te, - ParallelReadyList *ready_list) + binaryheap *ready_heap) { int i; @@ -4730,18 +4647,18 @@ reduce_dependencies(ArchiveHandle *AH, TocEntry *te, * the current restore pass, and it is currently a member of the * pending list (that check is needed to prevent double restore in * some cases where a list-file forces out-of-order restoring). - * However, if ready_list == NULL then caller doesn't want any list + * However, if ready_heap == NULL then caller doesn't want any list * memberships changed. */ if (otherte->depCount == 0 && _tocEntryRestorePass(otherte) == AH->restorePass && otherte->pending_prev != NULL && - ready_list != NULL) + ready_heap != NULL) { /* Remove it from pending list ... */ pending_list_remove(otherte); - /* ... and add to ready_list */ - ready_list_insert(ready_list, otherte); + /* ... and add to ready_heap */ + binaryheap_add(ready_heap, otherte); } } } -- 2.25.1 --gBBFr7Ir9EOA20Yy Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v8-0004-Remove-open-coded-binary-heap-in-pg_dump_sort.c.patch" ^ permalink raw reply [nested|flat] 3+ messages in thread
* [PATCH 4/6] Add CONCURRENTLY option to REPACK command. @ 2026-01-08 16:47 Antonin Houska <[email protected]> 0 siblings, 0 replies; 3+ messages in thread From: Antonin Houska @ 2026-01-08 16:47 UTC (permalink / raw) 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 should not request a 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 to write to 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 that we need to swap the files. (Of course, more data changes can take place while we are waiting for the lock - these will be applied to the new file after we have acquired the lock, 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 a 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. The WAL records produced by running DML commands on the new relation do not contain enough information to be processed by the logical decoding system. All we need from the new relation is the file (relfilenode), while the actual relation is eventually dropped. Thus there is no point in replaying the DMLs anywhere. --- doc/src/sgml/monitoring.sgml | 37 +- doc/src/sgml/mvcc.sgml | 12 +- doc/src/sgml/ref/repack.sgml | 129 +- src/Makefile | 1 + src/backend/access/heap/heapam.c | 34 +- src/backend/access/heap/heapam_handler.c | 227 ++- src/backend/access/heap/rewriteheap.c | 6 +- src/backend/catalog/system_views.sql | 19 +- src/backend/commands/cluster.c | 1647 +++++++++++++++-- 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/replication/logical/decode.c | 40 +- src/backend/replication/logical/snapbuild.c | 21 + .../replication/pgoutput_repack/Makefile | 32 + .../replication/pgoutput_repack/meson.build | 18 + .../pgoutput_repack/pgoutput_repack.c | 240 +++ .../storage/lmgr/generate-lwlocknames.pl | 2 +- src/backend/utils/time/snapmgr.c | 3 +- src/bin/psql/tab-complete.in.c | 4 +- src/include/access/heapam.h | 5 +- src/include/access/heapam_xlog.h | 2 + src/include/access/tableam.h | 10 + src/include/commands/cluster.h | 88 +- src/include/commands/progress.h | 17 +- src/include/replication/snapbuild.h | 1 + src/include/storage/lockdefs.h | 4 +- src/include/utils/snapmgr.h | 2 + src/test/modules/injection_points/Makefile | 3 + .../injection_points/expected/repack.out | 113 ++ .../expected/repack_toast.out | 64 + .../modules/injection_points/logical.conf | 1 + src/test/modules/injection_points/meson.build | 4 + .../injection_points/specs/repack.spec | 142 ++ .../injection_points/specs/repack_toast.spec | 105 ++ src/test/regress/expected/rules.out | 19 +- src/tools/pgindent/typedefs.list | 5 + 38 files changed, 2834 insertions(+), 239 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 create mode 100644 src/test/modules/injection_points/expected/repack.out create mode 100644 src/test/modules/injection_points/expected/repack_toast.out create mode 100644 src/test/modules/injection_points/logical.conf create mode 100644 src/test/modules/injection_points/specs/repack.spec create mode 100644 src/test/modules/injection_points/specs/repack_toast.spec diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index b07fe3294cd..ae56b09aeba 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -6202,14 +6202,35 @@ FROM pg_stat_get_backend_idset() AS backendid; <row> <entry role="catalog_table_entry"><para role="column_definition"> - <structfield>heap_tuples_written</structfield> <type>bigint</type> + <structfield>heap_tuples_inserted</structfield> <type>bigint</type> </para> <para> - Number of heap tuples written. + Number of heap tuples inserted. This counter only advances when the phase is <literal>seq scanning heap</literal>, - <literal>index scanning heap</literal> - or <literal>writing new heap</literal>. + <literal>index scanning heap</literal>, + <literal>writing new heap</literal> + or <literal>catch-up</literal>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>heap_tuples_updated</structfield> <type>bigint</type> + </para> + <para> + Number of heap tuples updated. + This counter only advances when the phase is <literal>catch-up</literal>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>heap_tuples_deleted</structfield> <type>bigint</type> + </para> + <para> + Number of heap tuples deleted. + This counter only advances when the phase is <literal>catch-up</literal>. </para></entry> </row> @@ -6290,6 +6311,14 @@ FROM pg_stat_get_backend_idset() AS backendid; <command>REPACK</command> is currently writing the new heap. </entry> </row> + <row> + <entry><literal>catch-up</literal></entry> + <entry> + <command>REPACK CONCURRENTLY</command> is currently processing the DML + commands that other transactions executed during any of the preceding + phases. + </entry> + </row> <row> <entry><literal>swapping relation files</literal></entry> <entry> diff --git a/doc/src/sgml/mvcc.sgml b/doc/src/sgml/mvcc.sgml index 049ee75a4ba..0f5c34af542 100644 --- a/doc/src/sgml/mvcc.sgml +++ b/doc/src/sgml/mvcc.sgml @@ -1833,15 +1833,17 @@ SELECT pg_advisory_lock(q.id) FROM <title>Caveats</title> <para> - Some DDL commands, currently only <link linkend="sql-truncate"><command>TRUNCATE</command></link> and the - table-rewriting forms of <link linkend="sql-altertable"><command>ALTER TABLE</command></link>, are not + Some commands, currently only <link linkend="sql-truncate"><command>TRUNCATE</command></link>, the + table-rewriting forms of <link linkend="sql-altertable"><command>ALTER + TABLE</command></link> and <command>REPACK</command> with + the <literal>CONCURRENTLY</literal> option, are not MVCC-safe. This means that after the truncation or rewrite commits, the table will appear empty to concurrent transactions, if they are using a - snapshot taken before the DDL command committed. This will only be an + snapshot taken before the command committed. This will only be an issue for a transaction that did not access the table in question - before the DDL command started — any transaction that has done so + before the command started — any transaction that has done so would hold at least an <literal>ACCESS SHARE</literal> table lock, - which would block the DDL command until that transaction completes. + which would block the truncating or rewriting command until that transaction completes. So these commands will not cause any apparent inconsistency in the table contents for successive queries on the target table, but they could cause visible inconsistency between the contents of the target diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml index 61d5c2cdef1..30c43c49069 100644 --- a/doc/src/sgml/ref/repack.sgml +++ b/doc/src/sgml/ref/repack.sgml @@ -28,6 +28,7 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING VERBOSE [ <replaceable class="parameter">boolean</replaceable> ] ANALYZE [ <replaceable class="parameter">boolean</replaceable> ] + CONCURRENTLY [ <replaceable class="parameter">boolean</replaceable> ] <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase> @@ -54,7 +55,8 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING processes every table and materialized view in the current database that the current user has the <literal>MAINTAIN</literal> privilege on. This form of <command>REPACK</command> cannot be executed inside a transaction - block. + block. Also, this form is not allowed if + the <literal>CONCURRENTLY</literal> option is used. </para> <para> @@ -67,7 +69,8 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING When a table is being repacked, an <literal>ACCESS EXCLUSIVE</literal> lock is acquired on it. This prevents any other database operations (both reads and writes) from operating on the table until the <command>REPACK</command> - is finished. + is finished. If you want to keep the table accessible during the repacking, + consider using the <literal>CONCURRENTLY</literal> option. </para> <refsect2 id="sql-repack-notes-on-clustering" xreflabel="Notes on Clustering"> @@ -195,6 +198,128 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING </listitem> </varlistentry> + <varlistentry> + <term><literal>CONCURRENTLY</literal></term> + <listitem> + <para> + Allow other transactions to use the table while it is being repacked. + </para> + + <para> + Internally, <command>REPACK</command> 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 <literal>ACCESS EXCLUSIVE</literal> 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. + </para> + + <para> + With the <literal>CONCURRENTLY</literal> option, the <literal>ACCESS + EXCLUSIVE</literal> 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 + (<xref linkend="logicaldecoding"/>) and applied before + the <literal>ACCESS EXCLUSIVE</literal> lock is requested. Thus the lock + is typically held only for the time needed to swap the files, which + should be pretty short. However, the time might still be noticeable if + too many data changes have been done to the table while + <command>REPACK</command> was waiting for the lock: those changes must + be processed just before the files are swapped, while the + <literal>ACCESS EXCLUSIVE</literal> lock is being held. + </para> + + <para> + Note that <command>REPACK</command> with the + <literal>CONCURRENTLY</literal> option does not try to order the rows + inserted into the table after the repacking started. Also + note <command>REPACK</command> might fail to complete due to DDL + commands executed on the table by other transactions during the + repacking. + </para> + + <note> + <para> + In addition to the temporary space requirements explained in + <xref linkend="sql-repack-notes-on-resources"/>, + the <literal>CONCURRENTLY</literal> 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 + <command>REPACK</command> has copied all the tuples from the old + file. Thus the tuples inserted into the old file during the copying are + also stored separately in a temporary file, so they can eventually be + applied to the new file. + </para> + + <para> + Furthermore, the data changes performed during the copying are + extracted from <link linkend="wal">write-ahead log</link> (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 <link linkend="guc-wal-segment-size"><varname>wal_segment_size</varname></link> + configuration parameter. + </para> + </note> + + <para> + The <literal>CONCURRENTLY</literal> option cannot be used in the + following cases: + + <itemizedlist> + <listitem> + <para> + The table is <literal>UNLOGGED</literal>. + </para> + </listitem> + + <listitem> + <para> + The table is partitioned. + </para> + </listitem> + + <listitem> + <para> + The table is a system catalog or a <acronym>TOAST</acronym> table. + </para> + </listitem> + + <listitem> + <para> + <command>REPACK</command> is executed inside a transaction block. + </para> + </listitem> + + <listitem> + <para> + The <link linkend="guc-wal-level"><varname>wal_level</varname></link> + configuration parameter is less than <literal>logical</literal>. + </para> + </listitem> + + <listitem> + <para> + The <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link> + configuration parameter does not allow for creation of an additional + replication slot. + </para> + </listitem> + </itemizedlist> + </para> + + <warning> + <para> + <command>REPACK</command> with the <literal>CONCURRENTLY</literal> + option is not MVCC-safe, see <xref linkend="mvcc-caveats"/> for + details. + </para> + </warning> + + </listitem> + </varlistentry> + <varlistentry> <term><literal>VERBOSE</literal></term> <listitem> diff --git a/src/Makefile b/src/Makefile index 2f31a2f20a7..b18c9a14ffa 100644 --- a/src/Makefile +++ b/src/Makefile @@ -23,6 +23,7 @@ SUBDIRS = \ interfaces \ backend/replication/libpqwalreceiver \ backend/replication/pgoutput \ + backend/replication/pgoutput_repack \ fe_utils \ bin \ pl \ diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index ad9d6338ec2..f1cdd5d1d87 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 walLogical); #ifdef USE_ASSERT_CHECKING static void check_lock_if_inplace_updateable_rel(Relation relation, const ItemPointerData *otid, @@ -2806,7 +2807,7 @@ xmax_infomask_changed(uint16 new_infomask, uint16 old_infomask) TM_Result heap_delete(Relation relation, const ItemPointerData *tid, CommandId cid, Snapshot crosscheck, bool wait, - TM_FailureData *tmfd, bool changingPart) + TM_FailureData *tmfd, bool changingPart, bool walLogical) { TM_Result result; TransactionId xid = GetCurrentTransactionId(); @@ -3053,7 +3054,8 @@ l1: * Compute replica identity tuple before entering the critical section so * we don't PANIC upon a memory allocation failure. */ - old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, &old_key_copied); + old_key_tuple = walLogical ? + ExtractReplicaIdentity(relation, &tp, true, &old_key_copied) : NULL; /* * If this is the first possibly-multixact-able operation in the current @@ -3143,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 (!walLogical) + xlrec.flags |= XLH_DELETE_NO_LOGICAL; + XLogBeginInsert(); XLogRegisterData(&xlrec, SizeOfHeapDelete); @@ -3235,7 +3246,8 @@ simple_heap_delete(Relation relation, const ItemPointerData *tid) result = heap_delete(relation, tid, GetCurrentCommandId(true), InvalidSnapshot, true /* wait for commit */ , - &tmfd, false /* changingPart */ ); + &tmfd, false, /* changingPart */ + true /* walLogical */ ); switch (result) { case TM_SelfModified: @@ -3276,7 +3288,7 @@ TM_Result heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup, CommandId cid, Snapshot crosscheck, bool wait, TM_FailureData *tmfd, LockTupleMode *lockmode, - TU_UpdateIndexes *update_indexes) + TU_UpdateIndexes *update_indexes, bool walLogical) { TM_Result result; TransactionId xid = GetCurrentTransactionId(); @@ -4169,7 +4181,8 @@ l2: newbuf, &oldtup, heaptup, old_key_tuple, all_visible_cleared, - all_visible_cleared_new); + all_visible_cleared_new, + walLogical); if (newbuf != buffer) { PageSetLSN(BufferGetPage(newbuf), recptr); @@ -4527,7 +4540,8 @@ simple_heap_update(Relation relation, const ItemPointerData *otid, HeapTuple tup result = heap_update(relation, otid, tup, GetCurrentCommandId(true), InvalidSnapshot, true /* wait for commit */ , - &tmfd, &lockmode, update_indexes); + &tmfd, &lockmode, update_indexes, + true /* walLogical */ ); switch (result) { case TM_SelfModified: @@ -8883,7 +8897,8 @@ static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf, Buffer newbuf, HeapTuple oldtup, HeapTuple newtup, HeapTuple old_key_tuple, - bool all_visible_cleared, bool new_all_visible_cleared) + bool all_visible_cleared, bool new_all_visible_cleared, + bool walLogical) { xl_heap_update xlrec; xl_heap_header xlhdr; @@ -8894,7 +8909,8 @@ 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 = RelationIsLogicallyLogged(reln) && + walLogical; bool init; int bufflags; diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 778377b9866..3526b6adcb5 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" @@ -309,7 +310,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, cid, crosscheck, wait, tmfd, changingPart, + true); } @@ -328,7 +330,7 @@ heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot, tuple->t_tableOid = slot->tts_tableOid; result = heap_update(relation, otid, tuple, cid, crosscheck, wait, - tmfd, lockmode, update_indexes); + tmfd, lockmode, update_indexes, true); ItemPointerCopy(&tuple->t_self, &slot->tts_tid); /* @@ -685,13 +687,15 @@ 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, double *tups_vacuumed, double *tups_recently_dead) { - RewriteState rwstate; + RewriteState rwstate = NULL; IndexScanDesc indexScan; TableScanDesc tableScan; HeapScanDesc heapScan; @@ -705,6 +709,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); @@ -720,9 +726,12 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, values = palloc_array(Datum, natts); isnull = palloc_array(bool, natts); - /* Initialize the rewrite operation */ - rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin, *xid_cutoff, - *multi_cutoff); + /* + * Initialize the rewrite operation. + */ + if (!concurrent) + rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin, + *xid_cutoff, *multi_cutoff); /* Set up sorting if wanted */ @@ -737,6 +746,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, * Prepare to scan the OldHeap. To ensure we see recently-dead tuples * that still need to be copied, we scan with SnapshotAny and use * HeapTupleSatisfiesVacuum for the visibility test. + * + * In the CONCURRENTLY case, we do regular MVCC visibility tests, using + * the snapshot passed by the caller. */ if (OldIndex != NULL && !use_sort) { @@ -753,7 +765,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, tableScan = NULL; heapScan = NULL; - indexScan = index_beginscan(OldHeap, OldIndex, SnapshotAny, NULL, 0, 0); + indexScan = index_beginscan(OldHeap, OldIndex, + snapshot ? snapshot : SnapshotAny, + NULL, 0, 0); index_rescan(indexScan, NULL, 0, NULL, 0); } else @@ -762,7 +776,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP); - tableScan = table_beginscan(OldHeap, SnapshotAny, 0, (ScanKey) NULL); + tableScan = table_beginscan(OldHeap, + snapshot ? snapshot : SnapshotAny, + 0, (ScanKey) NULL); heapScan = (HeapScanDesc) tableScan; indexScan = NULL; @@ -837,70 +853,77 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, tuple = ExecFetchSlotHeapTuple(slot, false, NULL); buf = hslot->buffer; - LockBuffer(buf, BUFFER_LOCK_SHARE); - - switch (HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf)) + /* + * Regarding CONCURRENTLY, see the comments on MVCC snapshot above. + */ + if (!concurrent) { - case HEAPTUPLE_DEAD: - /* Definitely dead */ - isdead = true; - break; - case HEAPTUPLE_RECENTLY_DEAD: - *tups_recently_dead += 1; - /* fall through */ - case HEAPTUPLE_LIVE: - /* Live or recently dead, must copy it */ - isdead = false; - break; - case HEAPTUPLE_INSERT_IN_PROGRESS: + LockBuffer(buf, BUFFER_LOCK_SHARE); - /* - * Since we hold exclusive lock on the relation, normally the - * only way to see this is if it was inserted earlier in our - * own transaction. However, it can happen in system - * catalogs, since we tend to release write lock before commit - * there. Give a warning if neither case applies; but in any - * case we had better copy it. - */ - if (!is_system_catalog && - !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data))) - elog(WARNING, "concurrent insert in progress within table \"%s\"", - RelationGetRelationName(OldHeap)); - /* treat as live */ - isdead = false; - break; - case HEAPTUPLE_DELETE_IN_PROGRESS: + switch (HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf)) + { + case HEAPTUPLE_DEAD: + /* Definitely dead */ + isdead = true; + break; + case HEAPTUPLE_RECENTLY_DEAD: + *tups_recently_dead += 1; + /* fall through */ + case HEAPTUPLE_LIVE: + /* Live or recently dead, must copy it */ + isdead = false; + break; + case HEAPTUPLE_INSERT_IN_PROGRESS: - /* - * Similar situation to INSERT_IN_PROGRESS case. - */ - if (!is_system_catalog && - !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple->t_data))) - elog(WARNING, "concurrent delete in progress within table \"%s\"", - RelationGetRelationName(OldHeap)); - /* treat as recently dead */ - *tups_recently_dead += 1; - isdead = false; - break; - default: - elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); - isdead = false; /* keep compiler quiet */ - break; - } + /* + * As long as we hold exclusive lock on the relation, + * normally the only way to see this is if it was inserted + * earlier in our own transaction. However, it can happen + * in system catalogs, since we tend to release write lock + * before commit there. Give a warning if neither case + * applies; but in any case we had better copy it. + */ + if (!is_system_catalog && + !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data))) + elog(WARNING, "concurrent insert in progress within table \"%s\"", + RelationGetRelationName(OldHeap)); + /* treat as live */ + isdead = false; + break; + case HEAPTUPLE_DELETE_IN_PROGRESS: + + /* + * Similar situation to INSERT_IN_PROGRESS case. + */ + if (!is_system_catalog && + !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple->t_data))) + elog(WARNING, "concurrent delete in progress within table \"%s\"", + RelationGetRelationName(OldHeap)); + /* treat as recently dead */ + *tups_recently_dead += 1; + isdead = false; + break; + default: + elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); + isdead = false; /* keep compiler quiet */ + break; + } - LockBuffer(buf, BUFFER_LOCK_UNLOCK); + LockBuffer(buf, BUFFER_LOCK_UNLOCK); - if (isdead) - { - *tups_vacuumed += 1; - /* heap rewrite module still needs to see it... */ - if (rewrite_heap_dead_tuple(rwstate, tuple)) + if (isdead) { - /* A previous recently-dead tuple is now known dead */ *tups_vacuumed += 1; - *tups_recently_dead -= 1; + /* heap rewrite module still needs to see it... */ + if (rewrite_heap_dead_tuple(rwstate, tuple)) + { + /* A previous recently-dead tuple is now known dead */ + *tups_vacuumed += 1; + *tups_recently_dead -= 1; + } + + continue; } - continue; } *num_tuples += 1; @@ -919,7 +942,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 +957,31 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, ct_val[1] = *num_tuples; pgstat_progress_update_multi_param(2, ct_index, ct_val); } + + /* + * 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,15 +1025,32 @@ 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); + + /* + * Try to keep the amount of not-yet-decoded WAL small, like + * above. + */ + 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; + } + } } tuplesort_end(tuplesort); } /* Write out any remaining tuples, and fsync if needed */ - end_heap_rewrite(rwstate); + if (rwstate) + end_heap_rewrite(rwstate); /* Clean up */ pfree(values); @@ -2376,6 +2441,10 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * SET WITHOUT OIDS. * * So, we must reconstruct the tuple from component Datums. + * + * If rwstate=NULL, use simple_heap_insert() instead of rewriting - in that + * case we still need to deform/form the tuple. TODO Shouldn't we rename the + * function, as might not do any rewrite? */ static void reform_and_rewrite_tuple(HeapTuple tuple, @@ -2398,8 +2467,28 @@ reform_and_rewrite_tuple(HeapTuple tuple, copiedTuple = heap_form_tuple(newTupDesc, values, isnull); - /* The heap rewrite module does the rest */ - rewrite_heap_tuple(rwstate, tuple, copiedTuple); + if (rwstate) + /* The heap rewrite module does the rest */ + rewrite_heap_tuple(rwstate, tuple, copiedTuple); + else + { + /* + * Insert tuple when processing REPACK CONCURRENTLY. + * + * rewriteheap.c is not used in the CONCURRENTLY case because it'd be + * difficult to do the same in the catch-up phase (as the logical + * decoding does not provide us with sufficient visibility + * information). Thus we must use heap_insert() both during the + * catch-up and here. + * + * The following is like simple_heap_insert() except that we pass the + * flag to skip logical decoding: as soon as REPACK CONCURRENTLY swaps + * the relation files, it drops this relation, so no logical + * replication subscription should need the data. + */ + heap_insert(NewHeap, copiedTuple, GetCurrentCommandId(true), + HEAP_INSERT_NO_LOGICAL, NULL); + } heap_freetuple(copiedTuple); } diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c index bae3a2da77a..45892da1e14 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/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 3f05ba3083a..d79eab5670c 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1298,16 +1298,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; @@ -1325,7 +1328,7 @@ CREATE VIEW pg_stat_progress_cluster AS phase, repack_index_relid AS cluster_index_relid, heap_tuples_scanned, - heap_tuples_written, + heap_tuples_inserted + heap_tuples_updated AS heap_tuples_written, heap_blks_total, heap_blks_scanned, index_rebuild_count diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index 094f3d36047..c3feb0c3de4 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -1,8 +1,23 @@ /*------------------------------------------------------------------------- * * cluster.c - * CLUSTER a table on an index. This is now also used for VACUUM FULL and - * REPACK. + * Implementation of REPACK [CONCURRENTLY], also known as CLUSTER and + * VACUUM FULL. + * + * There are two somewhat different ways to rewrite a table. In non- + * concurrent mode, it's easy: take AccessExclusiveLock, create a new + * transient relation, copy the tuples over to the relfilenode of the new + * relation, swap the relfilenodes, then drop the old relation. + * + * In concurrent mode, we lock the table with only ShareUpdateExclusiveLock, + * then do an initial copy as above. However, while the tuples are being + * copied, concurrent transactions could modify the table. To cope with those + * changes, we rely on logical decoding to obtain them from WAL. The changes + * are accumulated in a tuplestore. Once the initial copy is complete, we + * read the changes from the tuplestore and re-apply them on the new heap. + * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and + * swap the relfilenodes. This way, the time we hold a strong lock on the + * table is much reduced, and the bloat is eliminated. * * There is hardly anything left of Paul Brown's original implementation... * @@ -26,6 +41,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" @@ -33,6 +52,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" @@ -40,15 +60,21 @@ #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" #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" @@ -68,12 +94,62 @@ typedef struct Oid indexOid; } RelToCluster; +/* + * 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. + */ +static RelFileLocator repacked_rel_locator = {.relNumber = InvalidOid}; +static RelFileLocator repacked_rel_toast_locator = {.relNumber = InvalidOid}; + +/* + * Everything we need to call ExecInsertIndexTuples(). + */ +typedef struct IndexInsertState +{ + ResultRelInfo *rri; + EState *estate; +} IndexInsertState; + +/* The WAL segment being decoded. */ +static XLogSegNo repack_current_segment = 0; + +/* + * Information needed to apply concurrent data changes. + */ +typedef struct ChangeDest +{ + /* The relation the changes are applied to. */ + Relation rel; + + /* + * The following is needed to find the existing tuple if the change is + * UPDATE or DELETE. 'ident_key' should have all the fields except for + * 'sk_argument' initialized. + */ + Relation ident_index; + ScanKey ident_key; + int ident_key_nentries; + + /* Needed to update indexes of rel_dst. */ + IndexInsertState *iistate; +} ChangeDest; + static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, - Oid indexOid, Oid userid, int options); -static void rebuild_relation(Relation OldHeap, Relation index, bool verbose); + Oid indexOid, Oid userid, LOCKMODE lmode, + int options); +static void check_repack_concurrently_requirements(Relation rel); +static void rebuild_relation(Relation OldHeap, Relation index, bool verbose, + bool concurrent); static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, - bool verbose, bool *pSwapToastByContent, - TransactionId *pFreezeXid, MultiXactId *pCutoffMulti); + Snapshot snapshot, LogicalDecodingContext *decoding_ctx, + bool verbose, + bool *pSwapToastByContent, + TransactionId *pFreezeXid, + MultiXactId *pCutoffMulti); static List *get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt); static List *get_tables_to_repack_partitioned(RepackCommand cmd, @@ -81,13 +157,51 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd, MemoryContext permcxt); static bool cluster_is_permitted_for_relation(RepackCommand cmd, Oid relid, Oid userid); + +static void begin_concurrent_repack(Relation rel); +static void end_concurrent_repack(void); +static LogicalDecodingContext *setup_logical_decoding(Oid relid); +static HeapTuple get_changed_tuple(char *change); +static void apply_concurrent_changes(RepackDecodingState *dstate, + ChangeDest *dest); +static void apply_concurrent_insert(Relation rel, HeapTuple tup, + IndexInsertState *iistate, + TupleTableSlot *index_slot); +static void apply_concurrent_update(Relation rel, HeapTuple tup, + HeapTuple tup_target, + IndexInsertState *iistate, + TupleTableSlot *index_slot); +static void apply_concurrent_delete(Relation rel, HeapTuple tup_target); +static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest, + HeapTuple tup_key, + TupleTableSlot *ident_slot); +static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx, + XLogRecPtr end_of_wal, + ChangeDest *dest); +static IndexInsertState *get_index_insert_state(Relation relation, + Oid ident_index_id, + Relation *ident_index_p); +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 *decoding_ctx, + TransactionId frozenXid, + MultiXactId cutoffMulti); +static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); static Relation process_single_relation(RepackStmt *stmt, + LOCKMODE lockmode, + bool isTopLevel, ClusterParams *params); static Oid determine_clustered_index(Relation rel, bool usingindex, const char *indexname); static const char *RepackCommandAsString(RepackCommand cmd); +#define REPL_PLUGIN_NAME "pgoutput_repack" + /* * The repack code allows for processing multiple tables at once. Because * of this, we cannot just run everything on a single transaction, or we @@ -117,6 +231,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) ClusterParams params = {0}; Relation rel = NULL; MemoryContext repack_context; + LOCKMODE lockmode; List *rtcs; /* Parse option list */ @@ -127,6 +242,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) else if (strcmp(opt->defname, "analyze") == 0 || strcmp(opt->defname, "analyse") == 0) params.options |= defGetBoolean(opt) ? CLUOPT_ANALYZE : 0; + else if (strcmp(opt->defname, "concurrently") == 0 && + defGetBoolean(opt)) + { + if (stmt->command != REPACK_COMMAND_REPACK) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("CONCURRENTLY option not supported for %s", + RepackCommandAsString(stmt->command))); + params.options |= CLUOPT_CONCURRENT; + } else ereport(ERROR, errcode(ERRCODE_SYNTAX_ERROR), @@ -136,13 +261,25 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) parser_errposition(pstate, opt->location)); } + /* + * 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 a single relation is specified, process it and we're done ... unless * the relation is a partitioned table, in which case we fall through. */ if (stmt->relation != NULL) { - rel = process_single_relation(stmt, ¶ms); + rel = process_single_relation(stmt, lockmode, isTopLevel, ¶ms); if (rel == NULL) return; /* all done */ } @@ -157,10 +294,29 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) errmsg("cannot %s multiple tables", "REPACK (ANALYZE)")); /* - * By here, we know we are in a multi-table situation. In order to avoid - * holding locks for too long, we want to process each table in its own - * transaction. This forces us to disallow running inside a user - * transaction block. + * By here, we know we are in a multi-table situation. + * + * Concurrent processing is currently considered rather special (e.g. in + * terms of resources consumed) so it is not performed in bulk. + */ + if (params.options & CLUOPT_CONCURRENT) + { + if (rel != NULL) + { + Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + ereport(ERROR, + errmsg("REPACK CONCURRENTLY not supported for partitioned tables"), + errhint("Consider running the command for individual partitions.")); + } + else + ereport(ERROR, + errmsg("REPACK CONCURRENTLY requires explicit table name")); + } + + /* + * In order to avoid holding locks for too long, we want to process each + * table in its own transaction. This forces us to disallow running + * inside a user transaction block. */ PreventInTransactionBlock(isTopLevel, RepackCommandAsString(stmt->command)); @@ -243,7 +399,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) * Open the target table, coping with the case where it has been * dropped. */ - rel = try_table_open(rtc->tableOid, AccessExclusiveLock); + rel = try_table_open(rtc->tableOid, lockmode); if (rel == NULL) { CommitTransactionCommand(); @@ -254,7 +410,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) PushActiveSnapshot(GetTransactionSnapshot()); /* Process this table */ - cluster_rel(stmt->command, rel, rtc->indexOid, ¶ms); + cluster_rel(stmt->command, rel, rtc->indexOid, ¶ms, isTopLevel); /* cluster_rel closes the relation, but keeps lock */ PopActiveSnapshot(); @@ -283,22 +439,53 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) * If indexOid is InvalidOid, the table will be rewritten in physical order * instead of index order. * + * Note that, in the concurrent case, the function releases the lock at some + * point, in order to get AccessExclusiveLock for the final steps (i.e. to + * swap the relation files). To make things simpler, the caller should expect + * OldHeap to be closed on return, regardless CLUOPT_CONCURRENT. (The + * AccessExclusiveLock is kept till the end of the transaction.) + * * 'cmd' indicates which command is being executed, to be used for error * messages. */ void cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, - ClusterParams *params) + ClusterParams *params, bool isTopLevel) { Oid tableOid = RelationGetRelid(OldHeap); + Relation index; + LOCKMODE lmode; Oid save_userid; int save_sec_context; int save_nestlevel; bool verbose = ((params->options & CLUOPT_VERBOSE) != 0); bool recheck = ((params->options & CLUOPT_RECHECK) != 0); - Relation index; + bool concurrent = ((params->options & CLUOPT_CONCURRENT) != 0); + + /* + * 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(); @@ -324,10 +511,13 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, * If this is a single-transaction CLUSTER, we can skip these tests. We * *must* skip the one on indisclustered since it would reject an attempt * to cluster a not-previously-clustered index. + * + * XXX move [some of] these comments to where the RECHECK flag is + * determined? */ if (recheck && !cluster_rel_recheck(cmd, OldHeap, indexOid, save_userid, - params->options)) + lmode, params->options)) goto out; /* @@ -342,6 +532,12 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, errmsg("cannot run %s on a shared catalog", RepackCommandAsString(cmd))); + /* + * The CONCURRENTLY case should have been rejected earlier because it does + * not support system catalogs. + */ + Assert(!(OldHeap->rd_rel->relisshared && concurrent)); + /* * Don't process temp tables of other backends ... their local buffer * manager is not going to cope. @@ -362,7 +558,7 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, if (OidIsValid(indexOid)) { /* verify the index is good and lock it */ - check_index_is_clusterable(OldHeap, indexOid, AccessExclusiveLock); + check_index_is_clusterable(OldHeap, indexOid, lmode); /* also open it */ index = index_open(indexOid, NoLock); } @@ -397,7 +593,9 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, if (OldHeap->rd_rel->relkind == RELKIND_MATVIEW && !RelationIsPopulated(OldHeap)) { - relation_close(OldHeap, AccessExclusiveLock); + if (index) + index_close(index, lmode); + relation_close(OldHeap, lmode); goto out; } @@ -410,11 +608,34 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, * invalid, because we move tuples around. Promote them to relation * locks. Predicate locks on indexes will be promoted when they are * reindexed. + * + * During concurrent processing, the heap as well as its indexes stay in + * operation, so we postpone this step until they are locked using + * AccessExclusiveLock near the end of the processing. */ - TransferPredicateLocksToHeapRelation(OldHeap); + if (!concurrent) + TransferPredicateLocksToHeapRelation(OldHeap); /* rebuild_relation does all the dirty work */ - rebuild_relation(OldHeap, index, verbose); + PG_TRY(); + { + /* + * 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, concurrent); + } + PG_FINALLY(); + { + if (concurrent) + end_concurrent_repack(); + } + PG_END_TRY(); + /* rebuild_relation closes OldHeap, and index if valid */ out: @@ -433,14 +654,14 @@ out: */ static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, - Oid userid, int options) + Oid userid, LOCKMODE lmode, int options) { Oid tableOid = RelationGetRelid(OldHeap); /* Check that the user still has privileges for the relation */ if (!cluster_is_permitted_for_relation(cmd, tableOid, userid)) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } @@ -454,7 +675,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, */ if (RELATION_IS_OTHER_TEMP(OldHeap)) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } @@ -465,7 +686,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, */ if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid))) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } @@ -476,7 +697,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, if ((options & CLUOPT_RECHECK_ISCLUSTERED) != 0 && !get_index_isclustered(indexOid)) { - relation_close(OldHeap, AccessExclusiveLock); + relation_close(OldHeap, lmode); return false; } } @@ -488,7 +709,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, * Verify that the specified heap and index are valid to cluster on * * Side effect: obtains lock on the index. The caller may - * in some cases already have AccessExclusiveLock on the table, but + * in some cases already have a lock of the same strength on the table, but * not in all cases so we can't rely on the table-level lock for * protection here. */ @@ -617,18 +838,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) +rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent) { Oid tableOid = RelationGetRelid(OldHeap); Oid accessMethod = OldHeap->rd_rel->relam; @@ -636,13 +926,38 @@ 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; + LogicalDecodingContext *decoding_ctx = NULL; + Snapshot snapshot = NULL; +#if USE_ASSERT_CHECKING + LOCKMODE lmode; + + lmode = concurrent ? ShareUpdateExclusiveLock : AccessExclusiveLock; + + Assert(CheckRelationLockedByMe(OldHeap, lmode, false)); + Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false)); +#endif + + if (concurrent) + { + /* + * 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. + */ + decoding_ctx = setup_logical_decoding(tableOid); - Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false) && - (index == NULL || CheckRelationLockedByMe(index, AccessExclusiveLock, false))); + snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder); + PushActiveSnapshot(snapshot); + } /* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */ if (index != NULL) @@ -650,7 +965,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose) /* Remember info about rel before closing OldHeap */ relpersistence = OldHeap->rd_rel->relpersistence; - is_system_catalog = IsSystemRelation(OldHeap); /* * Create the transient table that will receive the re-ordered data. @@ -666,30 +980,54 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose) NewHeap = table_open(OIDNewHeap, NoLock); /* Copy the heap data into the new table in the desired order */ - copy_table_data(NewHeap, OldHeap, index, verbose, + copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose, &swap_toast_by_content, &frozenXid, &cutoffMulti); + /* The historic snapshot won't be needed anymore. */ + if (snapshot) + { + PopActiveSnapshot(); + UpdateActiveSnapshotCommandId(); + } - /* Close relcache entries, but keep lock until transaction commit */ - table_close(OldHeap, NoLock); - if (index) - index_close(index, NoLock); + if (concurrent) + { + Assert(!swap_toast_by_content); + rebuild_relation_finish_concurrent(NewHeap, OldHeap, index, + decoding_ctx, + frozenXid, cutoffMulti); - /* - * 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); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_FINAL_CLEANUP); - /* - * 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); + /* Done with decoding. */ + cleanup_logical_decoding(decoding_ctx); + } + else + { + bool is_system_catalog = IsSystemRelation(OldHeap); + + /* Close relcache entries, but keep lock until transaction commit */ + table_close(OldHeap, NoLock); + if (index) + index_close(index, NoLock); + + /* + * Close the new relation so it can be dropped as soon as the storage + * is swapped. The relation is not visible to others, so no need to + * unlock it explicitly. + */ + table_close(NewHeap, NoLock); + + /* + * Swap the physical files of the target and transient tables, then + * rebuild the target's indexes and throw away the transient table. + */ + finish_heap_swap(tableOid, OIDNewHeap, is_system_catalog, + swap_toast_by_content, false, true, true, + frozenXid, cutoffMulti, + relpersistence); + } } @@ -824,15 +1162,19 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod, /* * Do the physical copying of table data. * + * 'snapshot' and 'decoding_ctx': see table_relation_copy_for_cluster(). Pass + * iff concurrent processing is required. + * * There are three output parameters: * *pSwapToastByContent is set true if toast tables must be swapped by content. * *pFreezeXid receives the TransactionId used as freeze cutoff point. * *pCutoffMulti receives the MultiXactId used as a cutoff point. */ static void -copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verbose, - bool *pSwapToastByContent, TransactionId *pFreezeXid, - MultiXactId *pCutoffMulti) +copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, + Snapshot snapshot, LogicalDecodingContext *decoding_ctx, + bool verbose, bool *pSwapToastByContent, + TransactionId *pFreezeXid, MultiXactId *pCutoffMulti) { Relation relRelation; HeapTuple reltup; @@ -849,6 +1191,10 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb int elevel = verbose ? INFO : DEBUG2; PGRUsage ru0; char *nspname; + bool concurrent = snapshot != NULL; + LOCKMODE lmode; + + lmode = concurrent ? ShareUpdateExclusiveLock : AccessExclusiveLock; pg_rusage_init(&ru0); @@ -877,7 +1223,7 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb * will be held till end of transaction. */ if (OldHeap->rd_rel->reltoastrelid) - LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock); + LockRelationOid(OldHeap->rd_rel->reltoastrelid, lmode); /* * If both tables have TOAST tables, perform toast swap by content. It is @@ -886,7 +1232,8 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb * swap by links. This is okay because swap by content is only essential * for system catalogs, and we don't support schema changes for them. */ - if (OldHeap->rd_rel->reltoastrelid && NewHeap->rd_rel->reltoastrelid) + if (OldHeap->rd_rel->reltoastrelid && NewHeap->rd_rel->reltoastrelid && + !concurrent) { *pSwapToastByContent = true; @@ -907,6 +1254,10 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb * follow the toast pointers to the wrong place. (It would actually * work for values copied over from the old toast table, but not for * any values that we toast which were previously not toasted.) + * + * This would not work with CONCURRENTLY because we may need to delete + * TOASTed tuples from the new heap. With this hack, we'd delete them + * from the old heap. */ NewHeap->rd_toastoid = OldHeap->rd_rel->reltoastrelid; } @@ -982,7 +1333,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); @@ -991,7 +1344,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); @@ -1449,14 +1806,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 */ @@ -1482,39 +1838,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; + + /* 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); + reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params); + } /* Report that we are now doing clean up */ pgstat_progress_update_param(PROGRESS_REPACK_PHASE, @@ -1558,6 +1922,17 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, object.objectId = OIDNewHeap; object.objectSubId = 0; + if (!reindex) + { + /* + * Make sure the changes in pg_class are visible. This is especially + * important if !swap_toast_by_content, so that the correct TOAST + * relation is dropped. (reindex_relation() above did not help in this + * case)) + */ + CommandCounterIncrement(); + } + /* * The new relation is local to our transaction and we know nothing * depends on it, so DROP_RESTRICT should be OK. @@ -1597,7 +1972,7 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, /* Get the associated valid index to be renamed */ toastidx = toast_get_valid_index(newrel->rd_rel->reltoastrelid, - NoLock); + AccessExclusiveLock); /* rename the toast table ... */ snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u", @@ -1857,7 +2232,8 @@ cluster_is_permitted_for_relation(RepackCommand cmd, Oid relid, Oid userid) * case, if an index name is given, it's up to the caller to resolve it. */ static Relation -process_single_relation(RepackStmt *stmt, ClusterParams *params) +process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, + ClusterParams *params) { Relation rel; Oid tableOid; @@ -1866,13 +2242,9 @@ process_single_relation(RepackStmt *stmt, ClusterParams *params) Assert(stmt->command == REPACK_COMMAND_CLUSTER || stmt->command == REPACK_COMMAND_REPACK); - /* - * Find, lock, and check permissions on the table. We obtain - * AccessExclusiveLock right away to avoid lock-upgrade hazard in the - * single-transaction case. - */ + /* Find, lock, and check permissions on the table. */ tableOid = RangeVarGetRelidExtended(stmt->relation->relation, - AccessExclusiveLock, + lockmode, 0, RangeVarCallbackMaintainsTable, NULL); @@ -1904,13 +2276,14 @@ process_single_relation(RepackStmt *stmt, ClusterParams *params) return rel; else { - Oid indexOid; + Oid indexOid = InvalidOid; indexOid = determine_clustered_index(rel, stmt->usingindex, stmt->indexname); if (OidIsValid(indexOid)) - check_index_is_clusterable(rel, indexOid, AccessExclusiveLock); - cluster_rel(stmt->command, rel, indexOid, params); + check_index_is_clusterable(rel, indexOid, lockmode); + + cluster_rel(stmt->command, rel, indexOid, params, isTopLevel); /* Do an analyze, if requested */ if (params->options & CLUOPT_ANALYZE) @@ -1993,3 +2366,1067 @@ RepackCommandAsString(RepackCommand cmd) } return "???"; } + + +/* + * 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. + */ +static void +begin_concurrent_repack(Relation rel) +{ + Oid toastrelid; + + /* + * Avoid logical decoding of other relations by this backend. The lock we + * have guarantees that the actual locator cannot be changed concurrently: + * TRUNCATE needs AccessExclusiveLock. + */ + Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false)); + repacked_rel_locator = rel->rd_locator; + toastrelid = rel->rd_rel->reltoastrelid; + if (OidIsValid(toastrelid)) + { + Relation toastrel; + + /* Avoid logical decoding of other TOAST relations. */ + toastrel = table_open(toastrelid, AccessShareLock); + repacked_rel_toast_locator = toastrel->rd_locator; + table_close(toastrel, AccessShareLock); + } +} + +/* + * 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; +} + +/* + * Is this backend performing logical decoding on behalf of REPACK + * (CONCURRENTLY) ? + */ +bool +am_decoding_for_repack(void) +{ + return OidIsValid(repacked_rel_locator.relNumber); +} + +/* + * Does the WAL record contain a data change that this backend does not need + * to decode on behalf of REPACK (CONCURRENT)? + */ +bool +change_useless_for_repack(XLogRecordBuffer *buf) +{ + XLogReaderState *r = buf->record; + RelFileLocator locator; + + /* TOAST locator should not be set unless the main is. */ + Assert(!OidIsValid(repacked_rel_toast_locator.relNumber) || + OidIsValid(repacked_rel_locator.relNumber)); + + /* + * Backends not involved in REPACK (CONCURRENTLY) should not do the + * filtering. + */ + if (!am_decoding_for_repack()) + return false; + + /* + * If the record does not contain the block 0, it's probably not INSERT / + * UPDATE / DELETE. In any case, we do not have enough information to + * filter the change out. + */ + if (!XLogRecGetBlockTagExtended(r, 0, &locator, NULL, NULL, NULL)) + return false; + + /* + * Decode the change if it belongs to the table we are repacking, or if it + * belongs to its TOAST relation. + */ + if (RelFileLocatorEquals(locator, repacked_rel_locator)) + return false; + if (OidIsValid(repacked_rel_toast_locator.relNumber) && + RelFileLocatorEquals(locator, repacked_rel_toast_locator)) + return false; + + /* Filter out changes of other tables. */ + return true; +} + +/* + * 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) +{ + Relation rel; + TupleDesc tupdesc; + LogicalDecodingContext *ctx; + RepackDecodingState *dstate = palloc0_object(RepackDecodingState); + + /* + * REPACK CONCURRENTLY is not allowed in a transaction block, so this + * should never fire. + */ + Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny())); + + /* + * A single backend should not execute multiple REPACK commands at a time, + * so use PID to make the slot unique. + */ + snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid); + + /* + * Check if we can use logical decoding. + */ + CheckSlotPermissions(); + CheckLogicalDecodingRequirements(); + + /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */ + ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY, + false, false, false); + + /* + * Neither prepare_write nor do_write callback nor update_progress is + * useful for us. + */ + ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, + NIL, + true, + InvalidXLogRecPtr, + XL_ROUTINE(.page_read = read_local_xlog_page, + .segment_open = wal_segment_open, + .segment_close = wal_segment_close), + NULL, NULL, NULL); + + /* + * We don't have control on setting fast_forward, so at least check it. + */ + Assert(!ctx->fast_forward); + + 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->relid = relid; + dstate->tstore = tuplestore_begin_heap(false, false, + maintenance_work_mem); + + /* Caller should already have the table locked. */ + rel = table_open(relid, NoLock); + tupdesc = CreateTupleDescCopy(RelationGetDescr(rel)); + dstate->tupdesc = tupdesc; + table_close(rel, NoLock); + + /* 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; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + resowner_old = CurrentResourceOwner; + CurrentResourceOwner = dstate->resowner; + + PG_TRY(); + { + while (ctx->reader->EndRecPtr < end_of_wal) + { + XLogRecord *record; + XLogSegNo segno_new; + char *errm = NULL; + XLogRecPtr end_lsn; + + 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; + } + PG_CATCH(); + { + /* clear all timetravel entries */ + InvalidateSystemCaches(); + CurrentResourceOwner = resowner_old; + PG_RE_THROW(); + } + PG_END_TRY(); +} + +/* + * Apply changes stored in 'file'. + */ +static void +apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest) +{ + Relation rel = dest->rel; + 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); + + /* + * 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, tup, dest->iistate, index_slot); + + pfree(tup); + } + else if (change.kind == CHANGE_UPDATE_NEW || + change.kind == CHANGE_DELETE) + { + 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, dest, tup_key, ident_slot); + if (tup_exist == NULL) + elog(ERROR, "failed to find target tuple"); + + if (change.kind == CHANGE_UPDATE_NEW) + apply_concurrent_update(rel, tup, tup_exist, dest->iistate, + index_slot); + else + apply_concurrent_delete(rel, tup_exist); + + if (tup_old != NULL) + { + pfree(tup_old); + tup_old = NULL; + } + + pfree(tup); + } + else + elog(ERROR, "unrecognized kind of change: %d", change.kind); + + /* + * If a change was applied now, increment CID for next writes and + * update the snapshot so it sees the changes we've applied so far. + */ + 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, HeapTuple tup, IndexInsertState *iistate, + TupleTableSlot *index_slot) +{ + List *recheck; + + + /* + * Like simple_heap_insert(), but make sure that the INSERT is not + * logically decoded - see reform_and_rewrite_tuple() for more + * information. + */ + heap_insert(rel, tup, GetCurrentCommandId(true), HEAP_INSERT_NO_LOGICAL, + NULL); + + /* + * 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, + IndexInsertState *iistate, TupleTableSlot *index_slot) +{ + LockTupleMode lockmode; + TM_FailureData tmfd; + TU_UpdateIndexes update_indexes; + TM_Result res; + List *recheck; + + /* + * Write the new tuple into the new heap. ('tup' gets the TID assigned + * here.) + * + * Do it like in simple_heap_update(), except for 'wal_logical' (and + * except for 'wait'). + */ + res = heap_update(rel, &tup_target->t_self, tup, + GetCurrentCommandId(true), + InvalidSnapshot, + false, /* no wait - only we are doing changes */ + &tmfd, &lockmode, &update_indexes, + false /* wal_logical */ ); + if (res != TM_Ok) + ereport(ERROR, (errmsg("failed to apply concurrent UPDATE"))); + + 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) +{ + TM_Result res; + TM_FailureData tmfd; + + /* + * Delete tuple from the new heap. + * + * Do it like in simple_heap_delete(), except for 'wal_logical' (and + * except for 'wait'). + */ + res = heap_delete(rel, &tup_target->t_self, GetCurrentCommandId(true), + InvalidSnapshot, false, + &tmfd, + false, /* no wait - only we are doing changes */ + false /* wal_logical */ ); + + if (res != TM_Ok) + ereport(ERROR, (errmsg("failed to apply concurrent DELETE"))); + + pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_DELETED, 1); +} + +/* + * Find the tuple to be updated or deleted. + * + * '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, ChangeDest *dest, HeapTuple tup_key, + TupleTableSlot *ident_slot) +{ + Relation ident_index = dest->ident_index; + IndexScanDesc scan; + Form_pg_index ident_form; + int2vector *ident_indkey; + HeapTuple result = NULL; + + /* XXX no instrumentation for now */ + scan = index_beginscan(rel, ident_index, GetActiveSnapshot(), + NULL, dest->ident_key_nentries, 0); + + /* + * 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. + */ + index_rescan(scan, dest->ident_key, dest->ident_key_nentries, NULL, 0); + + /* Info needed to retrieve key values from heap tuple. */ + ident_form = 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); + } + index_endscan(scan); + + return result; +} + +/* + * Decode and apply concurrent changes. + */ +static void +process_concurrent_changes(LogicalDecodingContext *decoding_ctx, + XLogRecPtr end_of_wal, ChangeDest *dest) +{ + RepackDecodingState *dstate; + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_CATCH_UP); + + dstate = (RepackDecodingState *) decoding_ctx->output_writer_private; + + repack_decode_concurrent_changes(decoding_ctx, end_of_wal); + + if (dstate->nchanges == 0) + return; + + apply_concurrent_changes(dstate, dest); +} + +/* + * Initialize IndexInsertState for index specified by ident_index_id. + * + * While doing that, also return the identity index in *ident_index_p. + */ +static IndexInsertState * +get_index_insert_state(Relation relation, Oid ident_index_id, + Relation *ident_index_p) +{ + EState *estate; + int i; + IndexInsertState *result; + Relation ident_index = NULL; + + 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) + ident_index = ind_rel; + } + if (ident_index == NULL) + elog(ERROR, "failed to open identity index"); + + /* Only initialize fields needed by ExecInsertIndexTuples(). */ + result->estate = estate; + + *ident_index_p = ident_index; + 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); + + ReplicationSlotRelease(); + ReplicationSlotDrop(NameStr(dstate->slotname), false); + pfree(dstate); +} + +/* + * 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 *decoding_ctx, + 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; + XLogRecPtr wal_insert_ptr, + end_of_wal; + char dummy_rec_data = '\0'; + Relation *ind_refs, + *ind_refs_p; + int nind; + ChangeDest chgdst; + + /* 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. At the same time, we use ShareUpdateExclusiveLock + * to lock the existing indexes - that should be enough to prevent others + * from changing them while we're repacking the relation. The lock on + * table should prevent others from changing the index column list, but + * might not be enough for commands like ALTER INDEX ... SET ... (Those + * are not necessarily dangerous, but can make user confused if the + * changes they do get lost due to REPACK.) + */ + ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old); + + /* + * 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"))); + + /* Gather information to apply concurrent changes. */ + chgdst.rel = NewHeap; + chgdst.iistate = get_index_insert_state(NewHeap, ident_idx_new, + &chgdst.ident_index); + chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap, + &chgdst.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", NULL); + + /* + * Flush all WAL records inserted so far (possibly except for the last + * incomplete page, see GetInsertRecPtr), to minimize the amount of data + * we need to flush while holding exclusive lock on the source table. + */ + 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(decoding_ctx, end_of_wal, &chgdst); + + /* + * 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_oid(ind_oid, ind_oids_old) + { + Relation index; + + index = index_open(ind_oid, AccessExclusiveLock); + + /* + * TODO 1) Do we need to check if ALTER INDEX was executed since the + * new index was created in build_new_indexes()? 2) Specifically for + * the clustering index, should check_index_is_clusterable() be called + * here? (Not sure about the latter: ShareUpdateExclusiveLock on the + * table probably blocks all commands that affect the result of + * check_index_is_clusterable().) + */ + *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(decoding_ctx, end_of_wal, &chgdst); + + /* 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), + false, /* swap_toast_by_content */ + true, + InvalidTransactionId, + InvalidMultiXactId, + mapped_tables); + +#ifdef USE_ASSERT_CHECKING + + /* + * Concurrent processing is not supported for system relations, so + * there should be no mapped tables. + */ + for (int i = 0; i < 4; i++) + Assert(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(chgdst.ident_key); + free_index_insert_state(chgdst.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, + false, /* 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. The contained indexes end + * up locked using ShareUpdateExclusiveLock. + * + * A list of OIDs of the corresponding indexes created on NewHeap is + * returned. The order of items does match, so we can use these arrays to swap + * index storage. + */ +static List * +build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes) +{ + List *result = NIL; + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_REBUILD_INDEX); + + foreach_oid(ind_oid, OldIndexes) + { + Oid ind_oid_new; + char *newName; + Relation ind; + + ind = index_open(ind_oid, ShareUpdateExclusiveLock); + + newName = ChooseRelationName(get_rel_name(ind_oid), + NULL, + "repacknew", + get_rel_namespace(ind->rd_index->indrelid), + false); + ind_oid_new = index_create_copy(NewHeap, ind_oid, + ind->rd_rel->reltablespace, newName, + false); + result = lappend_oid(result, ind_oid_new); + + index_close(ind, NoLock); + } + + return result; +} diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 81a55a33ef2..ebc70f5bead 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -892,7 +892,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 f976c0e5c7e..296387c7889 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -6025,6 +6025,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 c363467a9cc..30a2b2578f7 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -126,7 +126,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); @@ -629,7 +629,8 @@ vacuum(List *relations, const VacuumParams params, BufferAccessStrategy bstrateg 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; } @@ -1999,7 +2000,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; @@ -2290,7 +2291,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params, /* VACUUM FULL is a variant of REPACK; see cluster.c */ cluster_rel(REPACK_COMMAND_VACUUMFULL, rel, InvalidOid, - &cluster_params); + &cluster_params, isTopLevel); /* cluster_rel closes the relation, but keeps lock */ rel = NULL; @@ -2333,7 +2334,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 de0a5fa1f7d..41f30a69dbd 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/replication/logical/decode.c b/src/backend/replication/logical/decode.c index e25dd6bc366..dc8c7be2aca 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,15 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) TransactionId xid = XLogRecGetXid(buf->record); SnapBuild *builder = ctx->snapshot_builder; + /* + * XXX Should we return here if change_useless_for_repack() returns true, + * instead of calling the function below? Unlike the fast-forward case, we + * shouldn't need the base snapshot for the containing transaction until + * we receive a change that belongs to the table being REPACKed. Thus it + * should be fine to skip SnapBuildProcessChange(), and therefore + * reorderbuffer.c can create the transaction later. + */ + ReorderBufferProcessXid(ctx->reorder, xid, buf->origptr); /* @@ -484,7 +494,8 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) { case XLOG_HEAP_INSERT: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeInsert(ctx, buf); break; @@ -496,17 +507,31 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) case XLOG_HEAP_HOT_UPDATE: case XLOG_HEAP_UPDATE: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeUpdate(ctx, buf); break; case XLOG_HEAP_DELETE: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeDelete(ctx, buf); break; case XLOG_HEAP_TRUNCATE: + /* Is REPACK (CONCURRENTLY) being run by this backend? */ + if (am_decoding_for_repack()) + { + /* + * TRUNCATE changes rd_locator of the relation, so it'd break + * REPACK (CONCURRENTLY). In fact it should not happen because + * TRUNCATE needs AccessExclusiveLock on the table. Should we + * only use Assert() here? + */ + ereport(ERROR, + (errmsg("TRUNCATE encountered while doing REPACK (CONCURRENTLY)"))); + } if (SnapBuildProcessChange(builder, xid, buf->origptr) && !ctx->fast_forward) DecodeTruncate(ctx, buf); @@ -1021,6 +1046,15 @@ DecodeDelete(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) xlrec = (xl_heap_delete *) XLogRecGetData(r); + /* + * Ignore changes which are considered useless for logical + * decoding. Currently such changes are created by REPACK (CONCURRENTLY) + * when replays DELETE commands on the new table (which is not yet visible + * to other transactions). + */ + if (xlrec->flags & XLH_DELETE_NO_LOGICAL) + return; + /* only interested in our database */ XLogRecGetBlockTag(r, 0, &target_locator, NULL, NULL); if (target_locator.dbOid != ctx->slot->data.database) diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c index 95f230f8e9b..e238bcd73cd 100644 --- a/src/backend/replication/logical/snapbuild.c +++ b/src/backend/replication/logical/snapbuild.c @@ -486,6 +486,27 @@ 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); + Assert(builder->building_full_snapshot); + + 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..c8930640a0d --- /dev/null +++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c @@ -0,0 +1,240 @@ +/*------------------------------------------------------------------------- + * + * pgoutput_repack.c + * Logical Replication output plugin for REPACK command + * + * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/replication/pgoutput_repack/pgoutput_repack.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/heaptoast.h" +#include "commands/cluster.h" +#include "replication/snapbuild.h" +#include "utils/memutils.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 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->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; + } +} + +/* 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; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + + size = VARHDRSZ + SizeOfConcurrentChange; + + /* + * 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; + if (size >= MaxAllocSize) + 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 = (char *) VARDATA(change_raw); + + /* + * Copy the tuple. + * + * Note: change->tup_data.t_data must be fixed on retrieval! + */ + memcpy(&change.tup_data, tuple, sizeof(HeapTupleData)); + memcpy(dst, &change, SizeOfConcurrentChange); + dst += SizeOfConcurrentChange; + memcpy(dst, tuple->t_data, tuple->t_len); + + /* The data has been copied. */ + if (flattened) + pfree(tuple); + + /* 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/lmgr/generate-lwlocknames.pl b/src/backend/storage/lmgr/generate-lwlocknames.pl index b49007167b0..2e7f1054e62 100644 --- a/src/backend/storage/lmgr/generate-lwlocknames.pl +++ b/src/backend/storage/lmgr/generate-lwlocknames.pl @@ -162,7 +162,7 @@ while (<$lwlocklist>) die "$wait_event_lwlocks[$lwlock_count] defined in wait_event_names.txt but " - . " missing from lwlocklist.h" + . "missing from lwlocklist.h" if $lwlock_count < scalar @wait_event_lwlocks; die diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c index 3af1b366adf..fdf3427b43f 100644 --- a/src/backend/utils/time/snapmgr.c +++ b/src/backend/utils/time/snapmgr.c @@ -214,7 +214,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 */ @@ -659,7 +658,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 2a1bb47ff03..0ec0f4c4790 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -5121,8 +5121,8 @@ match_previous_words(int pattern_id, * one word, so the above test is correct. */ if (ends_with(prev_wd, '(') || ends_with(prev_wd, ',')) - COMPLETE_WITH("ANALYZE", "VERBOSE"); - else if (TailMatches("ANALYZE", "VERBOSE")) + COMPLETE_WITH("ANALYZE", "CONCURRENTLY", "VERBOSE"); + else if (TailMatches("ANALYZE", "CONCURRENTLY", "VERBOSE")) COMPLETE_WITH("ON", "OFF"); } diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index ce48fac42ba..90cccea5bb5 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -361,14 +361,15 @@ extern void heap_multi_insert(Relation relation, TupleTableSlot **slots, BulkInsertState bistate); extern TM_Result heap_delete(Relation relation, const ItemPointerData *tid, CommandId cid, Snapshot crosscheck, bool wait, - TM_FailureData *tmfd, bool changingPart); + TM_FailureData *tmfd, bool changingPart, + bool wal_logical); extern void heap_finish_speculative(Relation relation, const ItemPointerData *tid); extern void heap_abort_speculative(Relation relation, const ItemPointerData *tid); extern TM_Result heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup, CommandId cid, Snapshot crosscheck, bool wait, 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 ce3566ba949..f1f5495556b 100644 --- a/src/include/access/heapam_xlog.h +++ b/src/include/access/heapam_xlog.h @@ -104,6 +104,8 @@ #define XLH_DELETE_CONTAINS_OLD_KEY (1<<2) #define XLH_DELETE_IS_SUPER (1<<3) #define XLH_DELETE_IS_PARTITION_MOVE (1<<4) +/* See heap_delete() */ +#define XLH_DELETE_NO_LOGICAL (1<<5) /* convenience macro for checking whether any form of old tuple was logged */ #define XLH_DELETE_CONTAINS_OLD \ diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h index e2ec5289d4d..76aa993009a 100644 --- a/src/include/access/tableam.h +++ b/src/include/access/tableam.h @@ -22,6 +22,7 @@ #include "access/xact.h" #include "commands/vacuum.h" #include "executor/tuptable.h" +#include "replication/logical.h" #include "storage/read_stream.h" #include "utils/rel.h" #include "utils/snapshot.h" @@ -629,6 +630,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, @@ -1646,6 +1649,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 @@ -1658,6 +1665,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, @@ -1666,6 +1675,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/commands/cluster.h b/src/include/commands/cluster.h index 28741988478..6a5c476294a 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/decode.h" +#include "replication/logical.h" #include "storage/lock.h" #include "utils/relcache.h" +#include "utils/resowner.h" +#include "utils/tuplestore.h" /* flag bits for ClusterParams->options */ @@ -25,6 +30,8 @@ #define CLUOPT_RECHECK_ISCLUSTERED 0x04 /* recheck relation state for * indisclustered */ #define CLUOPT_ANALYZE 0x08 /* do an ANALYZE */ +#define CLUOPT_CONCURRENT 0x10 /* allow concurrent data changes */ + /* options for CLUSTER */ typedef struct ClusterParams @@ -33,10 +40,84 @@ typedef struct ClusterParams } ClusterParams; +/* + * The following definitions are used by REPACK CONCURRENTLY. + */ + +typedef enum +{ + CHANGE_INSERT, + CHANGE_UPDATE_OLD, + CHANGE_UPDATE_NEW, + CHANGE_DELETE +} 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; + + /* Replication slot name. */ + NameData slotname; + + /* + * 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 ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel); extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid, - ClusterParams *params); + ClusterParams *params, bool isTopLevel); extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode); extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal); @@ -48,8 +129,13 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, bool swap_toast_by_content, bool check_constraints, bool is_internal, + bool reindex, TransactionId frozenXid, MultiXactId cutoffMulti, char newrelpersistence); +extern bool am_decoding_for_repack(void); +extern bool change_useless_for_repack(XLogRecordBuffer *buf); +extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx, + XLogRecPtr end_of_wal); #endif /* CLUSTER_H */ diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h index f00e39b937d..4445724a463 100644 --- a/src/include/commands/progress.h +++ b/src/include/commands/progress.h @@ -86,10 +86,12 @@ #define PROGRESS_REPACK_PHASE 1 #define PROGRESS_REPACK_INDEX_RELID 2 #define PROGRESS_REPACK_HEAP_TUPLES_SCANNED 3 -#define PROGRESS_REPACK_HEAP_TUPLES_WRITTEN 4 -#define PROGRESS_REPACK_TOTAL_HEAP_BLKS 5 -#define PROGRESS_REPACK_HEAP_BLKS_SCANNED 6 -#define PROGRESS_REPACK_INDEX_REBUILD_COUNT 7 +#define PROGRESS_REPACK_HEAP_TUPLES_INSERTED 4 +#define PROGRESS_REPACK_HEAP_TUPLES_UPDATED 5 +#define PROGRESS_REPACK_HEAP_TUPLES_DELETED 6 +#define PROGRESS_REPACK_TOTAL_HEAP_BLKS 7 +#define PROGRESS_REPACK_HEAP_BLKS_SCANNED 8 +#define PROGRESS_REPACK_INDEX_REBUILD_COUNT 9 /* * Phases of repack (as advertised via PROGRESS_REPACK_PHASE). @@ -98,9 +100,10 @@ #define PROGRESS_REPACK_PHASE_INDEX_SCAN_HEAP 2 #define PROGRESS_REPACK_PHASE_SORT_TUPLES 3 #define PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP 4 -#define PROGRESS_REPACK_PHASE_SWAP_REL_FILES 5 -#define PROGRESS_REPACK_PHASE_REBUILD_INDEX 6 -#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP 7 +#define PROGRESS_REPACK_PHASE_CATCH_UP 5 +#define PROGRESS_REPACK_PHASE_SWAP_REL_FILES 6 +#define PROGRESS_REPACK_PHASE_REBUILD_INDEX 7 +#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP 8 /* Progress parameters for CREATE INDEX */ /* 3, 4 and 5 reserved for "waitfor" metrics */ diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h index 34383dea776..5ee267d1c90 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 b73bb5618e6..3785b009808 100644 --- a/src/include/storage/lockdefs.h +++ b/src/include/storage/lockdefs.h @@ -36,8 +36,8 @@ typedef int LOCKMODE; #define AccessShareLock 1 /* SELECT */ #define RowShareLock 2 /* SELECT FOR UPDATE/FOR SHARE */ #define RowExclusiveLock 3 /* INSERT, UPDATE, DELETE */ -#define ShareUpdateExclusiveLock 4 /* VACUUM (non-FULL), ANALYZE, CREATE - * INDEX CONCURRENTLY */ +#define ShareUpdateExclusiveLock 4 /* VACUUM (non-exclusive), ANALYZE, CREATE + * INDEX CONCURRENTLY, REPACK CONCURRENTLY */ #define ShareLock 5 /* CREATE INDEX (WITHOUT CONCURRENTLY) */ #define ShareRowExclusiveLock 6 /* like EXCLUSIVE MODE, but allows ROW * SHARE */ diff --git a/src/include/utils/snapmgr.h b/src/include/utils/snapmgr.h index de824945f0b..0eb8ced76d3 100644 --- a/src/include/utils/snapmgr.h +++ b/src/include/utils/snapmgr.h @@ -64,6 +64,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/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index a41d781f8c9..6b6a3b84a57 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -14,8 +14,11 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress ISOLATION = basic \ inplace \ + repack \ + repack_toast \ syscache-update-pruned \ heap_lock_update +ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf # The injection points are cluster-wide, so disable installcheck NO_INSTALLCHECK = 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..b575e9052ee --- /dev/null +++ b/src/test/modules/injection_points/expected/repack.out @@ -0,0 +1,113 @@ +Parsed test spec with 2 sessions + +starting permutation: wait_before_lock change_existing change_new change_subxact1 change_subxact2 check2 wakeup_before_lock check1 +injection_points_attach +----------------------- + +(1 row) + +step wait_before_lock: + REPACK (CONCURRENTLY) repack_test USING INDEX repack_test_pkey; + <waiting ...> +step change_existing: + UPDATE repack_test SET i=10 where i=1; + UPDATE repack_test SET j=20 where i=2; + UPDATE repack_test SET i=30 where i=3; + UPDATE repack_test SET i=40 where i=30; + DELETE FROM repack_test WHERE i=4; + +step change_new: + INSERT INTO repack_test(i, j) VALUES (5, 5), (6, 6), (7, 7), (8, 8); + UPDATE repack_test SET i=50 where i=5; + UPDATE repack_test SET j=60 where i=6; + DELETE FROM repack_test WHERE i=7; + +step change_subxact1: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (100, 100); + SAVEPOINT s1; + UPDATE repack_test SET i=101 where i=100; + SAVEPOINT s2; + UPDATE repack_test SET i=102 where i=101; + COMMIT; + +step change_subxact2: + BEGIN; + SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 110); + ROLLBACK TO SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 111); + COMMIT; + +step check2: + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; + + i| j +---+--- + 2| 20 + 6| 60 + 8| 8 + 10| 1 + 40| 3 + 50| 5 +102|100 +110|111 +(8 rows) + +step wakeup_before_lock: + SELECT injection_points_wakeup('repack-concurrently-before-lock'); + +injection_points_wakeup +----------------------- + +(1 row) + +step wait_before_lock: <... completed> +step check1: + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; + +count +----- + 2 +(1 row) + + i| j +---+--- + 2| 20 + 6| 60 + 8| 8 + 10| 1 + 40| 3 + 50| 5 +102|100 +110|111 +(8 rows) + +count +----- + 0 +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/expected/repack_toast.out b/src/test/modules/injection_points/expected/repack_toast.out new file mode 100644 index 00000000000..4f866a74e32 --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_toast.out @@ -0,0 +1,64 @@ +Parsed test spec with 2 sessions + +starting permutation: wait_before_lock change check2 wakeup_before_lock check1 +injection_points_attach +----------------------- + +(1 row) + +step wait_before_lock: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step change: + UPDATE repack_test SET j=get_long_string() where i=2; + DELETE FROM repack_test WHERE i=3; + INSERT INTO repack_test(i, j) VALUES (4, get_long_string()); + +step check2: + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; + +step wakeup_before_lock: + SELECT injection_points_wakeup('repack-concurrently-before-lock'); + +injection_points_wakeup +----------------------- + +(1 row) + +step wait_before_lock: <... completed> +step check1: + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; + +count +----- + 4 +(1 row) + +count +----- + 0 +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/logical.conf b/src/test/modules/injection_points/logical.conf new file mode 100644 index 00000000000..e3d257315fa --- /dev/null +++ b/src/test/modules/injection_points/logical.conf @@ -0,0 +1 @@ +wal_level = logical diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index fcc85414515..bbf1f6e665c 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -45,11 +45,15 @@ tests += { 'specs': [ 'basic', 'inplace', + 'repack', + 'repack_toast', 'syscache-update-pruned', 'heap_lock_update', ], 'runningcheck': false, # see syscache-update-pruned # Some tests wait for all snapshots, so avoid parallel execution 'runningcheck-parallel': false, + # 'repack' requires wal_level = 'logical'. + 'regress_args': ['--temp-config', files('logical.conf')], }, } diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec new file mode 100644 index 00000000000..d727a9b056b --- /dev/null +++ b/src/test/modules/injection_points/specs/repack.spec @@ -0,0 +1,142 @@ +# REPACK (CONCURRENTLY) ... USING INDEX ...; +setup +{ + CREATE EXTENSION injection_points; + + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); + + CREATE TABLE relfilenodes(node oid); + + CREATE TABLE data_s1(i int, j int); + CREATE TABLE data_s2(i int, j int); +} + +teardown +{ + DROP TABLE repack_test; + DROP EXTENSION injection_points; + + DROP TABLE relfilenodes; + DROP TABLE data_s1; + DROP TABLE data_s2; +} + +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('repack-concurrently-before-lock', 'wait'); +} +# Perform the initial load and wait for s2 to do some data changes. +step wait_before_lock +{ + REPACK (CONCURRENTLY) repack_test USING INDEX repack_test_pkey; +} +# Check the table from the perspective of s1. +# +# Besides the contents, we also check that relfilenode has changed. + +# Have each session write the contents into a table and use FULL JOIN to check +# if the outputs are identical. +step check1 +{ + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; +} +teardown +{ + SELECT injection_points_detach('repack-concurrently-before-lock'); +} + +session s2 +# Change the existing data. UPDATE changes both key and non-key columns. Also +# update one row twice to test whether tuple version generated by this session +# can be found. +step change_existing +{ + UPDATE repack_test SET i=10 where i=1; + UPDATE repack_test SET j=20 where i=2; + UPDATE repack_test SET i=30 where i=3; + UPDATE repack_test SET i=40 where i=30; + DELETE FROM repack_test WHERE i=4; +} +# Insert new rows and UPDATE / DELETE some of them. Again, update both key and +# non-key column. +step change_new +{ + INSERT INTO repack_test(i, j) VALUES (5, 5), (6, 6), (7, 7), (8, 8); + UPDATE repack_test SET i=50 where i=5; + UPDATE repack_test SET j=60 where i=6; + DELETE FROM repack_test WHERE i=7; +} + +# When applying concurrent data changes, we should see the effects of an +# in-progress subtransaction. +# +# XXX Not sure this test is useful now - it was designed for the patch that +# preserves tuple visibility and which therefore modifies +# TransactionIdIsCurrentTransactionId(). +step change_subxact1 +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (100, 100); + SAVEPOINT s1; + UPDATE repack_test SET i=101 where i=100; + SAVEPOINT s2; + UPDATE repack_test SET i=102 where i=101; + COMMIT; +} + +# When applying concurrent data changes, we should not see the effects of a +# rolled back subtransaction. +# +# XXX Is this test useful? See above. +step change_subxact2 +{ + BEGIN; + SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 110); + ROLLBACK TO SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 111); + COMMIT; +} + +# Check the table from the perspective of s2. +step check2 +{ + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; +} +step wakeup_before_lock +{ + SELECT injection_points_wakeup('repack-concurrently-before-lock'); +} + +# Test if data changes introduced while one session is performing REPACK +# CONCURRENTLY find their way into the table. +permutation + wait_before_lock + change_existing + change_new + change_subxact1 + change_subxact2 + check2 + wakeup_before_lock + check1 diff --git a/src/test/modules/injection_points/specs/repack_toast.spec b/src/test/modules/injection_points/specs/repack_toast.spec new file mode 100644 index 00000000000..b48abf21450 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_toast.spec @@ -0,0 +1,105 @@ +# REPACK (CONCURRENTLY); +# +# Test handling of TOAST. At the same time, no tuplesort. +setup +{ + CREATE EXTENSION injection_points; + + -- Return a string that needs to be TOASTed. + CREATE FUNCTION get_long_string() + RETURNS text + LANGUAGE sql as $$ + SELECT string_agg(chr(65 + trunc(25 * random())::int), '') + FROM generate_series(1, 2048) s(x); + $$; + + CREATE TABLE repack_test(i int PRIMARY KEY, j text); + INSERT INTO repack_test(i, j) VALUES (1, get_long_string()), + (2, get_long_string()), (3, get_long_string()); + + CREATE TABLE relfilenodes(node oid); + + CREATE TABLE data_s1(i int, j text); + CREATE TABLE data_s2(i int, j text); +} + +teardown +{ + DROP TABLE repack_test; + DROP EXTENSION injection_points; + DROP FUNCTION get_long_string(); + + DROP TABLE relfilenodes; + DROP TABLE data_s1; + DROP TABLE data_s2; +} + +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('repack-concurrently-before-lock', 'wait'); +} +# Perform the initial load and wait for s2 to do some data changes. +step wait_before_lock +{ + REPACK (CONCURRENTLY) repack_test; +} +# Check the table from the perspective of s1. +# +# Besides the contents, we also check that relfilenode has changed. + +# Have each session write the contents into a table and use FULL JOIN to check +# if the outputs are identical. +step check1 +{ + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; +} +teardown +{ + SELECT injection_points_detach('repack-concurrently-before-lock'); +} + +session s2 +step change +{ + UPDATE repack_test SET j=get_long_string() where i=2; + DELETE FROM repack_test WHERE i=3; + INSERT INTO repack_test(i, j) VALUES (4, get_long_string()); +} +# Check the table from the perspective of s2. +step check2 +{ + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; +} +step wakeup_before_lock +{ + SELECT injection_points_wakeup('repack-concurrently-before-lock'); +} + +# Test if data changes introduced while one session is performing REPACK +# CONCURRENTLY find their way into the table. +permutation + wait_before_lock + change + check2 + wakeup_before_lock + check1 diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 48461550636..470920f0d16 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -2014,7 +2014,7 @@ pg_stat_progress_cluster| SELECT pid, phase, repack_index_relid AS cluster_index_relid, heap_tuples_scanned, - heap_tuples_written, + (heap_tuples_inserted + heap_tuples_updated) AS heap_tuples_written, heap_blks_total, heap_blks_scanned, index_rebuild_count @@ -2094,17 +2094,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 c81d93d0e5a..a0b7b38a5e2 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -419,6 +419,7 @@ CatCacheHeader CatalogId CatalogIdMapEntry CatalogIndexState +ChangeDest ChangeVarNodes_callback ChangeVarNodes_context CheckPoint @@ -495,6 +496,8 @@ CompressFileHandle CompressionLocation CompressorState ComputeXidHorizonsResult +ConcurrentChange +ConcurrentChangeKind ConditionVariable ConditionVariableMinimallyPadded ConditionalStack @@ -1274,6 +1277,7 @@ IndexElem IndexFetchHeapData IndexFetchTableData IndexInfo +IndexInsertState IndexList IndexOnlyScan IndexOnlyScanState @@ -2570,6 +2574,7 @@ ReorderBufferUpdateProgressTxnCB ReorderTuple RepOriginId RepackCommand +RepackDecodingState RepackStmt ReparameterizeForeignPathByChild_function ReplaceVarsFromTargetList_context -- 2.47.3 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v29-0005-Use-background-worker-to-do-logical-decoding.patch ^ permalink raw reply [nested|flat] 3+ messages in thread
end of thread, other threads:[~2026-01-08 16:47 UTC | newest] Thread overview: 3+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2019-07-06 14:39 [PATCH 2/2] review reworks Tomas Vondra <[email protected]> 2023-07-20 17:19 [PATCH v8 3/4] Convert pg_restore's ready_list to a priority queue. Nathan Bossart <[email protected]> 2026-01-08 16:47 [PATCH 4/6] Add CONCURRENTLY option to REPACK command. Antonin Houska <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox