agora inbox for [email protected]help / color / mirror / Atom feed
[PATCH 15/17] Make pg_upgrade aware of encryption. 24+ messages / 7 participants [nested] [flat]
* [PATCH 15/17] Make pg_upgrade aware of encryption. @ 2019-07-05 14:24 Antonin Houska <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Antonin Houska @ 2019-07-05 14:24 UTC (permalink / raw) This includes pg_resetwal utility becaue pg_upgrade calls it. --- src/bin/pg_resetwal/Makefile | 10 +++++-- src/bin/pg_resetwal/pg_resetwal.c | 51 +++++++++++++++++++++++++++++++++-- src/bin/pg_upgrade/controldata.c | 39 +++++++++++++++++++++++++++ src/bin/pg_upgrade/dump.c | 2 +- src/bin/pg_upgrade/exec.c | 30 +++++++++++++++++++-- src/bin/pg_upgrade/option.c | 10 ++++++- src/bin/pg_upgrade/parallel.c | 7 ++--- src/bin/pg_upgrade/pg_upgrade.c | 57 +++++++++++++++++++++++++++++++++------ src/bin/pg_upgrade/pg_upgrade.h | 9 ++++++- src/bin/pg_upgrade/server.c | 39 +++++++++++++++++++++++++-- src/fe_utils/encryption.c | 25 ++++++++++------- src/include/fe_utils/encryption.h | 1 + 12 files changed, 249 insertions(+), 31 deletions(-) diff --git a/src/bin/pg_resetwal/Makefile b/src/bin/pg_resetwal/Makefile index 2a3835691f..faebfda27e 100644 --- a/src/bin/pg_resetwal/Makefile +++ b/src/bin/pg_resetwal/Makefile @@ -15,13 +15,19 @@ subdir = src/bin/pg_resetwal top_builddir = ../../.. include $(top_builddir)/src/Makefile.global -OBJS= pg_resetwal.o $(WIN32RES) +override CPPFLAGS := -DFRONTEND $(CPPFLAGS) +LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport) + +OBJS= pg_resetwal.o encryption.o $(WIN32RES) all: pg_resetwal pg_resetwal: $(OBJS) | submake-libpgport $(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X) +encryption.c: % : $(top_srcdir)/src/backend/storage/file/% + rm -f $@ && $(LN_S) $< . + install: all installdirs $(INSTALL_PROGRAM) pg_resetwal$(X) '$(DESTDIR)$(bindir)/pg_resetwal$(X)' @@ -32,7 +38,7 @@ uninstall: rm -f '$(DESTDIR)$(bindir)/pg_resetwal$(X)' clean distclean maintainer-clean: - rm -f pg_resetwal$(X) $(OBJS) + rm -f pg_resetwal$(X) $(OBJS) encryption.c rm -rf tmp_check check: diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c index ff0f8ea5e7..43921132d4 100644 --- a/src/bin/pg_resetwal/pg_resetwal.c +++ b/src/bin/pg_resetwal/pg_resetwal.c @@ -54,11 +54,12 @@ #include "common/file_perm.h" #include "common/logging.h" #include "common/restricted_token.h" +#include "fe_utils/encryption.h" +#include "storage/encryption.h" #include "storage/large_object.h" #include "pg_getopt.h" #include "getopt_long.h" - static ControlFileData ControlFile; /* pg_control values */ static XLogSegNo newXlogSegNo; /* new XLOG segment # */ static bool guessed = false; /* T if we had to guess at any values */ @@ -135,7 +136,7 @@ main(int argc, char *argv[]) } - while ((c = getopt_long(argc, argv, "c:D:e:fl:m:no:O:x:", long_options, NULL)) != -1) + while ((c = getopt_long(argc, argv, "c:D:e:fK:l:m:no:O:x:", long_options, NULL)) != -1) { switch (c) { @@ -277,6 +278,12 @@ main(int argc, char *argv[]) } break; +#ifdef USE_ENCRYPTION + case 'K': + encryption_key_command = strdup(optarg); + break; +#endif /* USE_ENCRYPTION */ + case 'l': if (strspn(optarg, "01234567890ABCDEFabcdef") != XLOG_FNAME_LEN) { @@ -408,6 +415,28 @@ main(int argc, char *argv[]) XLogFromFileName(log_fname, &minXlogTli, &minXlogSegNo, WalSegSz); /* + * If the data is encrypted, we also might need to encrypt the XLOG record + * below. + */ + if (ControlFile.data_cipher > PG_CIPHER_NONE && !noupdate) + { + if (encryption_key_command) + run_encryption_key_command(encryption_key, DataDir); + else + { + /* + * If executed by pg_upgrade, we don't want pg_resetwal to run the + * encryption key command (possibly interactive application) w/o + * access to terminal. + */ + read_encryption_key_fe(stdin); + } + + setup_encryption(); + data_encrypted = true; + } + + /* * Also look at existing segment files to set up newXlogSegNo */ FindEndOfXLOG(); @@ -814,6 +843,12 @@ PrintControlValues(bool guessed) (ControlFile.float8ByVal ? _("by value") : _("by reference"))); printf(_("Data page checksum version: %u\n"), ControlFile.data_checksum_version); + if (ControlFile.data_cipher > PG_CIPHER_NONE) + printf(_("Data encryption fingerprint: %08X%08X%08X%08X\n"), + htonl(((uint32 *) ControlFile.encryption_verification)[0]), + htonl(((uint32 *) ControlFile.encryption_verification)[1]), + htonl(((uint32 *) ControlFile.encryption_verification)[2]), + htonl(((uint32 *) ControlFile.encryption_verification)[3])); } @@ -1164,6 +1199,14 @@ WriteEmptyXLOG(void) FIN_CRC32C(crc); record->xl_crc = crc; + if (data_encrypted) + { + char tweak[TWEAK_SIZE]; + + XLogEncryptionTweak(tweak, page->xlp_tli, newXlogSegNo, 0); + encrypt_block(buffer.data, buffer.data, XLOG_BLCKSZ, tweak, true); + } + /* Write the first page */ XLogFilePath(path, ControlFile.checkPointCopy.ThisTimeLineID, newXlogSegNo, WalSegSz); @@ -1224,6 +1267,10 @@ usage(void) printf(_(" [-D, --pgdata=]DATADIR data directory\n")); printf(_(" -e, --epoch=XIDEPOCH set next transaction ID epoch\n")); printf(_(" -f, --force force update to be done\n")); +#ifdef USE_ENCRYPTION + printf(_(" -K, --encryption-key-command\n" + " command that returns encryption key\n")); +#endif /* USE_ENCRYPTION */ printf(_(" -l, --next-wal-file=WALFILE set minimum starting location for new WAL\n")); printf(_(" -m, --multixact-ids=MXID,MXID set next and oldest multitransaction ID\n")); printf(_(" -n, --dry-run no update, just show what would be done\n")); diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c index 38236415be..dc15b979bc 100644 --- a/src/bin/pg_upgrade/controldata.c +++ b/src/bin/pg_upgrade/controldata.c @@ -485,6 +485,33 @@ get_control_data(ClusterInfo *cluster, bool live_check) cluster->controldata.data_checksum_version = str2uint(p); got_data_checksum_version = true; } + else if ((p = strstr(bufin, "encryption fingerprint")) != NULL) + { + int i; + + p = strchr(p, ':'); + + if (p == NULL || strlen(p) <= 1) + pg_fatal("%d: controldata retrieval problem\n", __LINE__); + + cluster->controldata.data_encrypted = true; + + /* Skip the colon and any whitespace after it */ + p = strchr(p, ':'); + if (p == NULL || strlen(p) <= 1) + pg_fatal("%d: controldata retrieval problem\n", __LINE__); + p = strpbrk(p, "01234567890ABCDEF"); + if (p == NULL || strlen(p) <= 1) + pg_fatal("%d: controldata retrieval problem\n", __LINE__); + + /* Make sure it looks like a valid finerprint */ + if (strspn(p, "0123456789ABCDEF") != 32) + pg_fatal("%d: controldata retrieval problem\n", __LINE__); + + for (i = 0; i < ENCRYPTION_SAMPLE_SIZE; i++) + sscanf(p + 2 * i, "%2hhx", + cluster->controldata.encryption_verification + i); + } } pclose(output); @@ -669,6 +696,18 @@ check_control_data(ControlData *oldctrl, pg_fatal("old cluster uses data checksums but the new one does not\n"); else if (oldctrl->data_checksum_version != newctrl->data_checksum_version) pg_fatal("old and new cluster pg_controldata checksum versions do not match\n"); + + if (oldctrl->data_encrypted && !newctrl->data_encrypted) + pg_fatal("old cluster is encrypted, but the new one is not\n"); + else if (!oldctrl->data_encrypted && newctrl->data_encrypted) + pg_fatal("old cluster is not encrypted, but the new one is\n"); + else if (oldctrl->data_encrypted) + { + if (memcmp(oldctrl->encryption_verification, + newctrl->encryption_verification, + ENCRYPTION_SAMPLE_SIZE) != 0) + pg_fatal("encryption of the new cluster is not compatible with encryption of the old one\n"); + } } diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c index c1429fe4bf..6fcf9126a4 100644 --- a/src/bin/pg_upgrade/dump.c +++ b/src/bin/pg_upgrade/dump.c @@ -22,7 +22,7 @@ generate_old_dump(void) prep_status("Creating dump of global objects"); /* run new pg_dumpall binary for globals */ - exec_prog(UTILITY_LOG_FILE, NULL, true, true, + exec_prog(UTILITY_LOG_FILE, NULL, true, true, NULL, "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers " "--binary-upgrade %s -f %s", new_cluster.bindir, cluster_conn_opts(&old_cluster), diff --git a/src/bin/pg_upgrade/exec.c b/src/bin/pg_upgrade/exec.c index 0363309328..cd6c68bdd9 100644 --- a/src/bin/pg_upgrade/exec.c +++ b/src/bin/pg_upgrade/exec.c @@ -74,11 +74,17 @@ get_bin_version(ClusterInfo *cluster) * If the command fails, an error message is optionally written to the specified * log_file, and the program optionally exits. * + * If encryption_key is passed, popen() is used and the key is sent to stdin + * of the command. + * * The code requires it be called first from the primary thread on Windows. + * + * TODO Consolidate the use of popen() with Windows. Or use only popen()? */ bool exec_prog(const char *log_file, const char *opt_log_file, - bool report_error, bool exit_on_error, const char *fmt,...) + bool report_error, bool exit_on_error, unsigned char *encryption_key, + const char *fmt,...) { int result = 0; int written; @@ -170,7 +176,27 @@ exec_prog(const char *log_file, const char *opt_log_file, /* see comment above */ if (mainThreadId == GetCurrentThreadId()) #endif - result = system(cmd); + { + if (!encryption_key) + result = system(cmd); + else + { + FILE *fp; + int i; + + fp = popen(cmd, "w"); + if (fp == NULL) + pg_fatal("Failed to execute \"%s\"\n", cmd); + + /* Send the key. */ + for (i = 0; i < ENCRYPTION_KEY_LENGTH; i++) + fprintf(fp, "%.2x", encryption_key[i]); + fputc('\n', fp); + + if (pclose(fp)) + pg_fatal("\"%s\" returned non-zero code\n", cmd); + } + } if (result != 0 && report_error) { diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c index 73f395f2a3..e2bb0e7c8e 100644 --- a/src/bin/pg_upgrade/option.c +++ b/src/bin/pg_upgrade/option.c @@ -56,6 +56,7 @@ parseCommandLine(int argc, char *argv[]) {"socketdir", required_argument, NULL, 's'}, {"verbose", no_argument, NULL, 'v'}, {"clone", no_argument, NULL, 1}, + {"encryption-key-command", required_argument, NULL, 'K'}, {NULL, 0, NULL, 0} }; @@ -101,7 +102,7 @@ parseCommandLine(int argc, char *argv[]) if (os_user_effective_id == 0) pg_fatal("%s: cannot be run as root\n", os_info.progname); - while ((option = getopt_long(argc, argv, "d:D:b:B:cj:ko:O:p:P:rs:U:v", + while ((option = getopt_long(argc, argv, "d:D:b:B:cj:K:ko:O:p:P:rs:U:v", long_options, &optindex)) != -1) { switch (option) @@ -209,6 +210,10 @@ parseCommandLine(int argc, char *argv[]) user_opts.transfer_mode = TRANSFER_MODE_CLONE; break; + case 'K': + encryption_key_command = pg_strdup(optarg); + break; + default: fprintf(stderr, _("Try \"%s --help\" for more information.\n"), os_info.progname); @@ -309,6 +314,9 @@ usage(void) printf(_(" -v, --verbose enable verbose internal logging\n")); printf(_(" -V, --version display version information, then exit\n")); printf(_(" --clone clone instead of copying files to new cluster\n")); +#ifdef USE_ENCRYPTION + printf(_(" -K, --encryption-key-command command that returns encryption key\n\n")); +#endif /* USE_ENCRYPTION */ printf(_(" -?, --help show this help, then exit\n")); printf(_("\n" "Before running pg_upgrade you must:\n" diff --git a/src/bin/pg_upgrade/parallel.c b/src/bin/pg_upgrade/parallel.c index 80ab1b8609..be3fd51f15 100644 --- a/src/bin/pg_upgrade/parallel.c +++ b/src/bin/pg_upgrade/parallel.c @@ -79,7 +79,7 @@ parallel_exec_prog(const char *log_file, const char *opt_log_file, if (user_opts.jobs <= 1) /* exit_on_error must be true to allow jobs */ - exec_prog(log_file, opt_log_file, true, true, "%s", cmd); + exec_prog(log_file, opt_log_file, true, true, NULL, "%s", cmd); else { /* parallel */ @@ -122,7 +122,8 @@ parallel_exec_prog(const char *log_file, const char *opt_log_file, child = fork(); if (child == 0) /* use _exit to skip atexit() functions */ - _exit(!exec_prog(log_file, opt_log_file, true, true, "%s", cmd)); + _exit(!exec_prog(log_file, opt_log_file, true, true, NULL, "%s", + cmd)); else if (child < 0) /* fork failed */ pg_fatal("could not create worker process: %s\n", strerror(errno)); @@ -160,7 +161,7 @@ win32_exec_prog(exec_thread_arg *args) { int ret; - ret = !exec_prog(args->log_file, args->opt_log_file, true, true, "%s", args->cmd); + ret = !exec_prog(args->log_file, args->opt_log_file, true, true, NULL, "%s", args->cmd); /* terminates thread */ return ret; diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c index 336df7378f..c464f90cf9 100644 --- a/src/bin/pg_upgrade/pg_upgrade.c +++ b/src/bin/pg_upgrade/pg_upgrade.c @@ -73,6 +73,12 @@ char *output_files[] = { NULL }; +/* + * Declare these locally so we don't have to link storage/file/encryption.c + * here. + */ +bool encryption_setup_done = false; +unsigned char encryption_key[ENCRYPTION_KEY_LENGTH]; int main(int argc, char **argv) @@ -94,6 +100,20 @@ main(int argc, char **argv) adjust_data_dir(&old_cluster); adjust_data_dir(&new_cluster); + /* + * The encryption key is needed to start the clusters. + */ + if (encryption_key_command) + { + /* + * Both clusters should have the same KDF parameters, so we can pass + * pgdata of any one. + */ + run_encryption_key_command(encryption_key, old_cluster.pgdata); + + encryption_setup_done = true; + } + setup(argv[0], &live_check); output_check_banner(live_check); @@ -139,6 +159,16 @@ main(int argc, char **argv) copy_xact_xlog_xid(); + if (encryption_setup_done) + { + /* + * Copy KDF file so that the old cluster encryption password works for + * the new cluster. + */ + read_kdf_file(old_cluster.pgdata); + write_kdf_file(new_cluster.pgdata); + } + /* New now using xids of the old system */ /* -- NEW -- */ @@ -170,13 +200,15 @@ main(int argc, char **argv) */ prep_status("Setting next OID for new cluster"); exec_prog(UTILITY_LOG_FILE, NULL, true, true, + encryption_setup_done ? encryption_key : NULL, "\"%s/pg_resetwal\" -o %u \"%s\"", - new_cluster.bindir, old_cluster.controldata.chkpnt_nxtoid, + new_cluster.bindir, + old_cluster.controldata.chkpnt_nxtoid, new_cluster.pgdata); check_ok(); prep_status("Sync data directory to disk"); - exec_prog(UTILITY_LOG_FILE, NULL, true, true, + exec_prog(UTILITY_LOG_FILE, NULL, true, true, NULL, "\"%s/initdb\" --sync-only \"%s\"", new_cluster.bindir, new_cluster.pgdata); check_ok(); @@ -270,7 +302,7 @@ prepare_new_cluster(void) * --analyze so autovacuum doesn't update statistics later */ prep_status("Analyzing all rows in the new cluster"); - exec_prog(UTILITY_LOG_FILE, NULL, true, true, + exec_prog(UTILITY_LOG_FILE, NULL, true, true, NULL, "\"%s/vacuumdb\" %s --all --analyze %s", new_cluster.bindir, cluster_conn_opts(&new_cluster), log_opts.verbose ? "--verbose" : ""); @@ -283,7 +315,7 @@ prepare_new_cluster(void) * counter later. */ prep_status("Freezing all rows in the new cluster"); - exec_prog(UTILITY_LOG_FILE, NULL, true, true, + exec_prog(UTILITY_LOG_FILE, NULL, true, true, NULL, "\"%s/vacuumdb\" %s --all --freeze %s", new_cluster.bindir, cluster_conn_opts(&new_cluster), log_opts.verbose ? "--verbose" : ""); @@ -304,7 +336,7 @@ prepare_new_globals(void) */ prep_status("Restoring global objects in the new cluster"); - exec_prog(UTILITY_LOG_FILE, NULL, true, true, + exec_prog(UTILITY_LOG_FILE, NULL, true, true, NULL, "\"%s/psql\" " EXEC_PSQL_ARGS " %s -f \"%s\"", new_cluster.bindir, cluster_conn_opts(&new_cluster), GLOBALS_DUMP_FILE); @@ -351,6 +383,7 @@ create_new_objects(void) NULL, true, true, + NULL, "\"%s/pg_restore\" %s %s --exit-on-error --verbose " "--dbname postgres \"%s\"", new_cluster.bindir, @@ -448,7 +481,7 @@ copy_subdir_files(const char *old_subdir, const char *new_subdir) prep_status("Copying old %s to new server", old_subdir); - exec_prog(UTILITY_LOG_FILE, NULL, true, true, + exec_prog(UTILITY_LOG_FILE, NULL, true, true, NULL, #ifndef WIN32 "cp -Rf \"%s\" \"%s\"", #else @@ -475,15 +508,20 @@ copy_xact_xlog_xid(void) /* set the next transaction id and epoch of the new cluster */ prep_status("Setting next transaction ID and epoch for new cluster"); exec_prog(UTILITY_LOG_FILE, NULL, true, true, + encryption_setup_done ? encryption_key : NULL, "\"%s/pg_resetwal\" -f -x %u \"%s\"", - new_cluster.bindir, old_cluster.controldata.chkpnt_nxtxid, + new_cluster.bindir, + old_cluster.controldata.chkpnt_nxtxid, new_cluster.pgdata); exec_prog(UTILITY_LOG_FILE, NULL, true, true, + encryption_setup_done ? encryption_key : NULL, "\"%s/pg_resetwal\" -f -e %u \"%s\"", - new_cluster.bindir, old_cluster.controldata.chkpnt_nxtepoch, + new_cluster.bindir, + old_cluster.controldata.chkpnt_nxtepoch, new_cluster.pgdata); /* must reset commit timestamp limits also */ exec_prog(UTILITY_LOG_FILE, NULL, true, true, + encryption_setup_done ? encryption_key : NULL, "\"%s/pg_resetwal\" -f -c %u,%u \"%s\"", new_cluster.bindir, old_cluster.controldata.chkpnt_nxtxid, @@ -510,6 +548,7 @@ copy_xact_xlog_xid(void) * counters here and the oldest multi present on system. */ exec_prog(UTILITY_LOG_FILE, NULL, true, true, + encryption_setup_done ? encryption_key : NULL, "\"%s/pg_resetwal\" -O %u -m %u,%u \"%s\"", new_cluster.bindir, old_cluster.controldata.chkpnt_nxtmxoff, @@ -538,6 +577,7 @@ copy_xact_xlog_xid(void) * next=MaxMultiXactId, but multixact.c can cope with that just fine. */ exec_prog(UTILITY_LOG_FILE, NULL, true, true, + encryption_setup_done ? encryption_key : NULL, "\"%s/pg_resetwal\" -m %u,%u \"%s\"", new_cluster.bindir, old_cluster.controldata.chkpnt_nxtmulti + 1, @@ -549,6 +589,7 @@ copy_xact_xlog_xid(void) /* now reset the wal archives in the new cluster */ prep_status("Resetting WAL archives"); exec_prog(UTILITY_LOG_FILE, NULL, true, true, + encryption_setup_done ? encryption_key : NULL, /* use timeline 1 to match controldata and no WAL history file */ "\"%s/pg_resetwal\" -l 00000001%s \"%s\"", new_cluster.bindir, old_cluster.controldata.nextxlogfile + 8, diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h index 5d31750d86..e89f6c08a0 100644 --- a/src/bin/pg_upgrade/pg_upgrade.h +++ b/src/bin/pg_upgrade/pg_upgrade.h @@ -10,7 +10,10 @@ #include <sys/stat.h> #include <sys/time.h> +#include "fe_utils/encryption.h" #include "libpq-fe.h" +#include "storage/relfilenode.h" +#include "storage/encryption.h" /* Use port in the private/dynamic port number range */ #define DEF_PGUPORT 50432 @@ -227,6 +230,8 @@ typedef struct bool date_is_int; bool float8_pass_by_value; bool data_checksum_version; + bool data_encrypted; + uint8 encryption_verification[ENCRYPTION_SAMPLE_SIZE]; } ControlData; /* @@ -367,7 +372,9 @@ void generate_old_dump(void); #define EXEC_PSQL_ARGS "--echo-queries --set ON_ERROR_STOP=on --no-psqlrc --dbname=template1" bool exec_prog(const char *log_file, const char *opt_log_file, - bool report_error, bool exit_on_error, const char *fmt,...) pg_attribute_printf(5, 6); + bool report_error, bool exit_on_error, + unsigned char *encryption_key, const char *fmt,...) + pg_attribute_printf(6, 7); void verify_directories(void); bool pid_lock_file_exists(const char *datadir); diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c index 6ad4b14e16..93cb518cc5 100644 --- a/src/bin/pg_upgrade/server.c +++ b/src/bin/pg_upgrade/server.c @@ -252,6 +252,41 @@ start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error) cluster->pgopts ? cluster->pgopts : "", socket_string); /* + * If encryption key needs to be sent, run a separate process now and let + * it send the password to the postmaster. We cannot send the key later + * in the current process because the exec_prog call below blocks until + * the postmaster succeeds or fails to start (and it will definitely fail + * if it receives no key). + */ + if (encryption_setup_done) + { +#ifndef WIN32 + pid_t sender; + + sender = fork(); + if (sender == 0) + { + char port_str[6]; + + snprintf(port_str, sizeof(port_str), "%d", cluster->port); + + /* in child process */ + send_key_to_postmaster(cluster->sockdir, port_str, + encryption_key); + exit(EXIT_SUCCESS); + } + else if (sender < 0) + { + pg_fatal("could not create key sender process"); + exit(EXIT_FAILURE); + } +#else + /* TODO */ + #error "W32 not implemented yet" +#endif + } + + /* * Don't throw an error right away, let connecting throw the error because * it might supply a reason for the failure. */ @@ -260,7 +295,7 @@ start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error) (strcmp(SERVER_LOG_FILE, SERVER_START_LOG_FILE) != 0) ? SERVER_LOG_FILE : NULL, - report_and_exit_on_error, false, + report_and_exit_on_error, false, NULL, "%s", cmd); /* Did it fail and we are just testing if the server could be started? */ @@ -336,7 +371,7 @@ stop_postmaster(bool in_atexit) else return; /* no cluster running */ - exec_prog(SERVER_STOP_LOG_FILE, NULL, !in_atexit, !in_atexit, + exec_prog(SERVER_STOP_LOG_FILE, NULL, !in_atexit, !in_atexit, NULL, "\"%s/pg_ctl\" -w -D \"%s\" -o \"%s\" %s stop", cluster->bindir, cluster->pgconfig, cluster->pgopts ? cluster->pgopts : "", diff --git a/src/fe_utils/encryption.c b/src/fe_utils/encryption.c index 134b3bde9b..2b4a6fadad 100644 --- a/src/fe_utils/encryption.c +++ b/src/fe_utils/encryption.c @@ -259,8 +259,6 @@ run_encryption_key_command(unsigned char *encryption_key, char *data_dir) FILE *fp; char cmd[MAXPGPATH]; char *sp, *dp, *endp; - char *buf; - int read_len, i, c; Assert(encryption_key_command != NULL && strlen(encryption_key_command) > 0); @@ -312,15 +310,25 @@ run_encryption_key_command(unsigned char *encryption_key, char *data_dir) exit(EXIT_FAILURE); } + /* Read the key. */ + read_encryption_key_fe(fp); + + pclose(fp); +} + +/* + * Frontend counterpart of read_encryption_key(). + */ +void +read_encryption_key_fe(FILE *f) +{ + char *buf; + int read_len, i, c; + buf = (char *) palloc(ENCRYPTION_KEY_CHARS); - /* - * Read the key. This is very similar to backend's read_encryption_key() - * but there seems to be no straightforward way to call the function from - * here. - */ read_len = 0; - while ((c = fgetc(fp)) != EOF && c != '\n') + while ((c = fgetc(f)) != EOF && c != '\n') { if (read_len >= ENCRYPTION_KEY_CHARS) { @@ -349,7 +357,6 @@ run_encryption_key_command(unsigned char *encryption_key, char *data_dir) } pfree(buf); - pclose(fp); } /* diff --git a/src/include/fe_utils/encryption.h b/src/include/fe_utils/encryption.h index da302fe494..2c6032df07 100644 --- a/src/include/fe_utils/encryption.h +++ b/src/include/fe_utils/encryption.h @@ -22,5 +22,6 @@ extern void derive_key_from_password(unsigned char *encryption_key, const char *password, int len); extern void run_encryption_key_command(unsigned char *encryption_key, char *data_dir); +extern void read_encryption_key_fe(FILE *f); extern bool send_key_to_postmaster(const char *host, const char *port, const unsigned char *encryption_Key); -- 2.13.7 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v04-0016-Teach-the-remaining-frontend-application-about-encry.patch ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH v6 5/7] Row pattern recognition patch (docs). @ 2023-09-12 05:22 Tatsuo Ishii <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Tatsuo Ishii @ 2023-09-12 05:22 UTC (permalink / raw) --- doc/src/sgml/advanced.sgml | 52 ++++++++++++++++++++++++++++++++++ doc/src/sgml/func.sgml | 54 ++++++++++++++++++++++++++++++++++++ doc/src/sgml/ref/select.sgml | 38 +++++++++++++++++++++++-- 3 files changed, 142 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml index 755c9f1485..eda3612822 100644 --- a/doc/src/sgml/advanced.sgml +++ b/doc/src/sgml/advanced.sgml @@ -537,6 +537,58 @@ WHERE pos < 3; <literal>rank</literal> less than 3. </para> + <para> + Row pattern common syntax can be used with row pattern common syntax to + perform row pattern recognition in a query. Row pattern common syntax + includes two sub clauses. <literal>DEFINE</literal> defines definition + variables along with an expression. The expression must be a logical + expression, which means it must + return <literal>TRUE</literal>, <literal>FALSE</literal> + or <literal>NULL</literal>. Moreover if the expression comprises a column + reference, it must be the argument of <function>rpr</function>. An example + of <literal>DEFINE</literal> is as follows. + +<programlisting> +DEFINE + LOWPRICE AS price <= 100, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +</programlisting> + + Note that <function>PREV</function> returns price column in the previous + row if it's called in a context of row pattern recognition. So in the + second line means the definition variable "UP" is <literal>TRUE</literal> + when price column in the current row is greater than the price column in + the previous row. Likewise, "DOWN" is <literal>TRUE</literal> when when + price column in the current row is lower than the price column in the + previous row. + </para> + <para> + Once <literal>DEFINE</literal> exists, <literal>PATTERN</literal> can be + used. <literal>PATTERN</literal> defines a sequence of rows that satisfies + certain conditions. For example following <literal>PATTERN</literal> + defines that a row starts with the condition "LOWPRICE", then one or more + rows satisfy "UP" and finally one or more rows satisfy "DOWN". If a + sequence of rows found, rpr returns the column at the starting row. + Example of a <literal>SELECT</literal> using the <literal>DEFINE</literal> + and <literal>PATTERN</literal> clause is as follows. + +<programlisting> +SELECT company, tdate, price, max(price) OVER w FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (LOWPRICE UP+ DOWN+) + DEFINE + LOWPRICE AS price <= 100, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); +</programlisting> + </para> + <para> When a query involves multiple window functions, it is possible to write out each one with a separate <literal>OVER</literal> clause, but this is diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 24ad87f910..9c99dda4ae 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -21780,6 +21780,7 @@ SELECT count(*) FROM sometable; returns <literal>NULL</literal> if there is no such row. </para></entry> </row> + </tbody> </tgroup> </table> @@ -21819,6 +21820,59 @@ SELECT count(*) FROM sometable; Other frame specifications can be used to obtain other effects. </para> + <para> + Row pattern recognition navigation functions are listed in + <xref linkend="functions-rpr-navigation-table"/>. These functions + can be used to describe DEFINE clause of Row pattern recognition. + </para> + + <table id="functions-rpr-navigation-table"> + <title>Row Pattern Navigation Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>prev</primary> + </indexterm> + <function>prev</function> ( <parameter>value</parameter> <type>anyelement</type> ) + <returnvalue>anyelement</returnvalue> + </para> + <para> + Returns the column value at the previous row; + returns NULL if there is no previous row in the window frame. + </para></entry> + </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>next</primary> + </indexterm> + <function>next</function> ( <parameter>value</parameter> <type>anyelement</type> ) + <returnvalue>anyelement</returnvalue> + </para> + <para> + Returns the column value at the next row; + returns NULL if there is no next row in the window frame. + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + <note> <para> The SQL standard defines a <literal>RESPECT NULLS</literal> or diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml index 0ee0cc7e64..8d3becd57a 100644 --- a/doc/src/sgml/ref/select.sgml +++ b/doc/src/sgml/ref/select.sgml @@ -966,8 +966,8 @@ WINDOW <replaceable class="parameter">window_name</replaceable> AS ( <replaceabl The <replaceable class="parameter">frame_clause</replaceable> can be one of <synopsis> -{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable>frame_exclusion</replaceable> ] -{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable>frame_exclusion</replaceable> ] +{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable>frame_exclusion</replaceable> ] [row_pattern_common_syntax] +{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable>frame_exclusion</replaceable> ] [row_pattern_common_syntax] </synopsis> where <replaceable>frame_start</replaceable> @@ -1074,6 +1074,40 @@ EXCLUDE NO OTHERS a given peer group will be in the frame or excluded from it. </para> + <para> + The + optional <replaceable class="parameter">row_pattern_common_syntax</replaceable> + defines the <firstterm>row pattern recognition condition</firstterm> for + this + window. <replaceable class="parameter">row_pattern_common_syntax</replaceable> + includes following subclauses. <literal>AFTER MATCH SKIP PAST LAST + ROW</literal> or <literal>AFTER MATCH SKIP TO NEXT ROW</literal> controls + how to proceed to next row position after a match + found. With <literal>AFTER MATCH SKIP PAST LAST ROW</literal> (the + default) next row position is next to the last row of previous match. On + the other hand, with <literal>AFTER MATCH SKIP TO NEXT ROW</literal> next + row position is always next to the last row of previous + match. <literal>DEFINE</literal> defines definition variables along with a + boolean expression. <literal>PATTERN</literal> defines a sequence of rows + that satisfies certain conditions using variables defined + in <literal>DEFINE</literal> clause. If the variable is not defined in + the <literal>DEFINE</literal> clause, it is implicitly assumed + following is defined in the <literal>DEFINE</literal> clause. + +<synopsis> +<literal>variable_name</literal> AS TRUE +</synopsis> + + Note that the maximu number of variables defined + in <literal>DEFINE</literal> clause is 26. + +<synopsis> +[ AFTER MATCH SKIP PAST LAST ROW | AFTER MATCH SKIP TO NEXT ROW ] +PATTERN <replaceable class="parameter">pattern_variable_name</replaceable>[+] [, ...] +DEFINE <replaceable class="parameter">definition_varible_name</replaceable> AS <replaceable class="parameter">expression</replaceable> [, ...] +</synopsis> + </para> + <para> The purpose of a <literal>WINDOW</literal> clause is to specify the behavior of <firstterm>window functions</firstterm> appearing in the query's -- 2.25.1 ----Next_Part(Tue_Sep_12_15_18_43_2023_359)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v6-0006-Row-pattern-recognition-patch-tests.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 1/6] Add REPACK command @ 2026-01-22 08:23 Antonin Houska <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Antonin Houska @ 2026-01-22 08:23 UTC (permalink / raw) REPACK absorbs the functionality of VACUUM FULL and CLUSTER in a single command. Because this functionality is completely different from regular VACUUM, having it separate from VACUUM makes it easier for users to understand; as for CLUSTER, the term is heavily overloaded in the IT world and even in Postgres itself, so it's good that we can avoid it. This also adds pg_repackdb, a new utility that can invoke the new commands. This is heavily based on vacuumdb. Author: Antonin Houska <[email protected]> Co-authored-by: =C3=81lvaro Herrera <[email protected]> Reviewed-by: Mihail Nikalayeu <[email protected]> Reviewed-by: Robert Treat <[email protected]> Reviewed-by: Euler Taveira <[email protected]> Reviewed-by: Matheus Alcantara <[email protected]> Reviewed-by: Junwang Zhao <[email protected]> Reviewed-by: jian he <[email protected]> Discussion: https://postgr.es/m/82651.1720540558@antos Discussion: https://postgr.es/m/[email protected] --- doc/src/sgml/monitoring.sgml | 223 +++++- doc/src/sgml/ref/allfiles.sgml | 2 + doc/src/sgml/ref/cluster.sgml | 97 +-- doc/src/sgml/ref/clusterdb.sgml | 5 + doc/src/sgml/ref/pg_repackdb.sgml | 488 +++++++++++++ doc/src/sgml/ref/repack.sgml | 328 +++++++++ doc/src/sgml/ref/vacuum.sgml | 33 +- doc/src/sgml/reference.sgml | 2 + src/backend/access/heap/heapam_handler.c | 32 +- src/backend/catalog/index.c | 2 +- src/backend/catalog/system_views.sql | 29 +- src/backend/commands/cluster.c | 842 +++++++++++++++-------- src/backend/commands/vacuum.c | 6 +- src/backend/parser/gram.y | 86 ++- src/backend/tcop/utility.c | 23 +- src/backend/utils/adt/pgstatfuncs.c | 4 +- src/bin/psql/tab-complete.in.c | 42 +- src/bin/scripts/Makefile | 3 + src/bin/scripts/meson.build | 2 + src/bin/scripts/pg_repackdb.c | 240 +++++++ src/bin/scripts/t/103_repackdb.pl | 47 ++ src/bin/scripts/vacuuming.c | 102 ++- src/bin/scripts/vacuuming.h | 3 + src/include/commands/cluster.h | 8 +- src/include/commands/progress.h | 50 +- src/include/nodes/parsenodes.h | 35 +- src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + src/include/utils/backend_progress.h | 2 +- src/test/regress/expected/cluster.out | 134 +++- src/test/regress/expected/rules.out | 72 +- src/test/regress/sql/cluster.sql | 70 +- src/tools/pgindent/typedefs.list | 2 + 33 files changed, 2480 insertions(+), 536 deletions(-) create mode 100644 doc/src/sgml/ref/pg_repackdb.sgml create mode 100644 doc/src/sgml/ref/repack.sgml create mode 100644 src/bin/scripts/pg_repackdb.c create mode 100644 src/bin/scripts/t/103_repackdb.pl diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 817fd9f4ca7..b07fe3294cd 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -405,6 +405,14 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss = 11:34 0:00 postgres: ser </entry> </row> =20 + <row> + <entry><structname>pg_stat_progress_repack</structname><indexterm><p= rimary>pg_stat_progress_repack</primary></indexterm></entry> + <entry>One row for each backend running + <command>REPACK</command>, showing current progress. See + <xref linkend=3D"repack-progress-reporting"/>. + </entry> + </row> + <row> <entry><structname>pg_stat_progress_basebackup</structname><indexter= m><primary>pg_stat_progress_basebackup</primary></indexterm></entry> <entry>One row for each WAL sender process streaming a base backup, @@ -5609,7 +5617,8 @@ FROM pg_stat_get_backend_idset() AS backendid; certain commands during command execution. Currently, the only commands which support progress reporting are <command>ANALYZE</command>, <command>CLUSTER</command>, - <command>CREATE INDEX</command>, <command>VACUUM</command>, + <command>CREATE INDEX</command>, <command>REPACK</command>, + <command>VACUUM</command>, <command>COPY</command>, and <xref linkend=3D"protocol-replication-base-backup"/> (i.e., replica= tion command that <xref linkend=3D"app-pgbasebackup"/> issues to take @@ -6093,6 +6102,218 @@ FROM pg_stat_get_backend_idset() AS backendid; </table> </sect2> =20 + <sect2 id=3D"repack-progress-reporting"> + <title>REPACK Progress Reporting</title> + + <indexterm> + <primary>pg_stat_progress_repack</primary> + </indexterm> + + <para> + Whenever <command>REPACK</command> is running, + the <structname>pg_stat_progress_repack</structname> view will contain a + row for each backend that is currently running the command. The tables + below describe the information that will be reported and provide + information about how to interpret it. + </para> + + <table id=3D"pg-stat-progress-repack-view" xreflabel=3D"pg_stat_progress= _repack"> + <title><structname>pg_stat_progress_repack</structname> View</title> + <tgroup cols=3D"1"> + <thead> + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + <structfield>pid</structfield> <type>integer</type> + </para> + <para> + Process ID of backend. + </para></entry> + </row> + + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + <structfield>datid</structfield> <type>oid</type> + </para> + <para> + OID of the database to which this backend is connected. + </para></entry> + </row> + + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + <structfield>datname</structfield> <type>name</type> + </para> + <para> + Name of the database to which this backend is connected. + </para></entry> + </row> + + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + <structfield>relid</structfield> <type>oid</type> + </para> + <para> + OID of the table being repacked. + </para></entry> + </row> + + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + <structfield>phase</structfield> <type>text</type> + </para> + <para> + Current processing phase. See <xref linkend=3D"repack-phases"/>. + </para></entry> + </row> + + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + <structfield>repack_index_relid</structfield> <type>oid</type> + </para> + <para> + If the table is being scanned using an index, this is the OID of the + index being used; otherwise, it is zero. + </para></entry> + </row> + + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + <structfield>heap_tuples_scanned</structfield> <type>bigint</type> + </para> + <para> + Number of heap tuples scanned. + This counter only advances when the phase is + <literal>seq scanning heap</literal>, + <literal>index scanning heap</literal> + or <literal>writing new heap</literal>. + </para></entry> + </row> + + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + <structfield>heap_tuples_written</structfield> <type>bigint</type> + </para> + <para> + Number of heap tuples written. + This counter only advances when the phase is + <literal>seq scanning heap</literal>, + <literal>index scanning heap</literal> + or <literal>writing new heap</literal>. + </para></entry> + </row> + + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + <structfield>heap_blks_total</structfield> <type>bigint</type> + </para> + <para> + Total number of heap blocks in the table. This number is reported + as of the beginning of <literal>seq scanning heap</literal>. + </para></entry> + </row> + + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + <structfield>heap_blks_scanned</structfield> <type>bigint</type> + </para> + <para> + Number of heap blocks scanned. This counter only advances when the + phase is <literal>seq scanning heap</literal>. + </para></entry> + </row> + + <row> + <entry role=3D"catalog_table_entry"><para role=3D"column_definition"> + <structfield>index_rebuild_count</structfield> <type>bigint</type> + </para> + <para> + Number of indexes rebuilt. This counter only advances when the pha= se + is <literal>rebuilding index</literal>. + </para></entry> + </row> + </tbody> + </tgroup> + </table> + + <table id=3D"repack-phases"> + <title>REPACK Phases</title> + <tgroup cols=3D"2"> + <colspec colname=3D"col1" colwidth=3D"1*"/> + <colspec colname=3D"col2" colwidth=3D"2*"/> + <thead> + <row> + <entry>Phase</entry> + <entry>Description</entry> + </row> + </thead> + + <tbody> + <row> + <entry><literal>initializing</literal></entry> + <entry> + The command is preparing to begin scanning the heap. This phase is + expected to be very brief. + </entry> + </row> + <row> + <entry><literal>seq scanning heap</literal></entry> + <entry> + The command is currently scanning the table using a sequential scan. + </entry> + </row> + <row> + <entry><literal>index scanning heap</literal></entry> + <entry> + <command>REPACK</command> is currently scanning the table using an = index scan. + </entry> + </row> + <row> + <entry><literal>sorting tuples</literal></entry> + <entry> + <command>REPACK</command> is currently sorting tuples. + </entry> + </row> + <row> + <entry><literal>writing new heap</literal></entry> + <entry> + <command>REPACK</command> is currently writing the new heap. + </entry> + </row> + <row> + <entry><literal>swapping relation files</literal></entry> + <entry> + The command is currently swapping newly-built files into place. + </entry> + </row> + <row> + <entry><literal>rebuilding index</literal></entry> + <entry> + The command is currently rebuilding an index. + </entry> + </row> + <row> + <entry><literal>performing final cleanup</literal></entry> + <entry> + The command is performing final cleanup. When this phase is + completed, <command>REPACK</command> will end. + </entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> + <sect2 id=3D"copy-progress-reporting"> <title>COPY Progress Reporting</title> =20 diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index e167406c744..5df944d13ca 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -167,6 +167,7 @@ Complete list of usable sgml source files in this direc= tory. <!ENTITY refreshMaterializedView SYSTEM "refresh_materialized_view.sgml"> <!ENTITY reindex SYSTEM "reindex.sgml"> <!ENTITY releaseSavepoint SYSTEM "release_savepoint.sgml"> +<!ENTITY repack SYSTEM "repack.sgml"> <!ENTITY reset SYSTEM "reset.sgml"> <!ENTITY revoke SYSTEM "revoke.sgml"> <!ENTITY rollback SYSTEM "rollback.sgml"> @@ -213,6 +214,7 @@ Complete list of usable sgml source files in this direc= tory. <!ENTITY pgIsready SYSTEM "pg_isready.sgml"> <!ENTITY pgReceivewal SYSTEM "pg_receivewal.sgml"> <!ENTITY pgRecvlogical SYSTEM "pg_recvlogical.sgml"> +<!ENTITY pgRepackdb SYSTEM "pg_repackdb.sgml"> <!ENTITY pgResetwal SYSTEM "pg_resetwal.sgml"> <!ENTITY pgRestore SYSTEM "pg_restore.sgml"> <!ENTITY pgRewind SYSTEM "pg_rewind.sgml"> diff --git a/doc/src/sgml/ref/cluster.sgml b/doc/src/sgml/ref/cluster.sgml index 0b47460080b..2cda711bc9f 100644 --- a/doc/src/sgml/ref/cluster.sgml +++ b/doc/src/sgml/ref/cluster.sgml @@ -33,51 +33,13 @@ CLUSTER [ ( <replaceable class=3D"parameter">option</re= placeable> [, ...] ) ] [ <r <title>Description</title> =20 <para> - <command>CLUSTER</command> instructs <productname>PostgreSQL</productna= me> - to cluster the table specified - by <replaceable class=3D"parameter">table_name</replaceable> - based on the index specified by - <replaceable class=3D"parameter">index_name</replaceable>. The index mu= st - already have been defined on - <replaceable class=3D"parameter">table_name</replaceable>. + The <command>CLUSTER</command> command is equivalent to + <xref linkend=3D"sql-repack"/> with an <literal>USING INDEX</literal> + clause. See there for more details. </para> =20 - <para> - When a table is clustered, it is physically reordered - based on the index information. Clustering is a one-time operation: - when the table is subsequently updated, the changes are - not clustered. That is, no attempt is made to store new or - updated rows according to their index order. (If one wishes, one can - periodically recluster by issuing the command again. Also, setting - the table's <literal>fillfactor</literal> storage parameter to less than - 100% can aid in preserving cluster ordering during updates, since updat= ed - rows are kept on the same page if enough space is available there.) - </para> - - <para> - When a table is clustered, <productname>PostgreSQL</productname> - remembers which index it was clustered by. The form - <command>CLUSTER <replaceable class=3D"parameter">table_name</replaceab= le></command> - reclusters the table using the same index as before. You can also - use the <literal>CLUSTER</literal> or <literal>SET WITHOUT CLUSTER</lit= eral> - forms of <link linkend=3D"sql-altertable"><command>ALTER TABLE</command= ></link> to set the index to be used for - future cluster operations, or to clear any previous setting. - </para> - - <para> - <command>CLUSTER</command> without a - <replaceable class=3D"parameter">table_name</replaceable> reclusters al= l the - previously-clustered tables in the current database that the calling us= er - has privileges for. This form of <command>CLUSTER</command> cannot be - executed inside a transaction block. - </para> +<!-- Do we need to describe exactly which options map to what? They seem = obvious to me. --> =20 - <para> - When a table is being clustered, 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>CLUSTER</command> is finished. - </para> </refsect1> =20 <refsect1> @@ -136,63 +98,12 @@ CLUSTER [ ( <replaceable class=3D"parameter">option</r= eplaceable> [, ...] ) ] [ <r on the table. </para> =20 - <para> - In cases where you are accessing single rows randomly - within a table, the actual order of the data in the - table is unimportant. However, if you tend to access some - data more than others, and there is an index that groups - them together, you will benefit from using <command>CLUSTER</command>. - If you are requesting a range of indexed values from a table, or a - single indexed value that has multiple rows that match, - <command>CLUSTER</command> will help because once the index identifies= the - table page for the first row that matches, all other rows - that match are probably already on the same table page, - and so you save disk accesses and speed up the query. - </para> - - <para> - <command>CLUSTER</command> can re-sort the table using either an index= scan - on the specified index, or (if the index is a b-tree) a sequential - scan followed by sorting. It will attempt to choose the method that - will be faster, based on planner cost parameters and available statist= ical - information. - </para> - <para> While <command>CLUSTER</command> is running, the <xref linkend=3D"guc-search-path"/> is temporarily changed to <literal>pg_ca= talog, pg_temp</literal>. </para> =20 - <para> - When an index scan is used, a temporary copy of the table is created t= hat - contains the table data in the index order. Temporary copies of each - index on the table are created as well. Therefore, you need free spac= e on - disk at least equal to the sum of the table size and the index sizes. - </para> - - <para> - When a sequential scan and sort is used, a temporary sort file is - also created, so that the peak temporary space requirement is as much - as double the table size, plus the index sizes. This method is often - faster than the index scan method, but if the disk space requirement is - intolerable, you can disable this choice by temporarily setting <xref - linkend=3D"guc-enable-sort"/> to <literal>off</literal>. - </para> - - <para> - It is advisable to set <xref linkend=3D"guc-maintenance-work-mem"/> to - a reasonably large value (but not more than the amount of RAM you can - dedicate to the <command>CLUSTER</command> operation) before clusterin= g. - </para> - - <para> - Because the planner records statistics about the ordering of - tables, it is advisable to run <link linkend=3D"sql-analyze"><command>= ANALYZE</command></link> - on the newly clustered table. - Otherwise, the planner might make poor choices of query plans. - </para> - <para> Because <command>CLUSTER</command> remembers which indexes are cluster= ed, one can cluster the tables one wants clustered manually the first time, diff --git a/doc/src/sgml/ref/clusterdb.sgml b/doc/src/sgml/ref/clusterdb.s= gml index 0d2051bf6f1..b50c9581a98 100644 --- a/doc/src/sgml/ref/clusterdb.sgml +++ b/doc/src/sgml/ref/clusterdb.sgml @@ -64,6 +64,11 @@ PostgreSQL documentation this utility and via other methods for accessing the server. </para> =20 + <para> + <application>clusterdb</application> has been superseded by + <application>pg_repackdb</application>. + </para> + </refsect1> =20 =20 diff --git a/doc/src/sgml/ref/pg_repackdb.sgml b/doc/src/sgml/ref/pg_repack= db.sgml new file mode 100644 index 00000000000..a5ea513c6b1 --- /dev/null +++ b/doc/src/sgml/ref/pg_repackdb.sgml @@ -0,0 +1,488 @@ +<!-- +doc/src/sgml/ref/pg_repackdb.sgml +PostgreSQL documentation +--> + +<refentry id=3D"app-pgrepackdb"> + <indexterm zone=3D"app-pgrepackdb"> + <primary>pg_repackdb</primary> + </indexterm> + + <refmeta> + <refentrytitle><application>pg_repackdb</application></refentrytitle> + <manvolnum>1</manvolnum> + <refmiscinfo>Application</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>pg_repackdb</refname> + <refpurpose>repack and analyze a <productname>PostgreSQL</productname> + database</refpurpose> + </refnamediv> + + <refsynopsisdiv> + <cmdsynopsis> + <command>pg_repackdb</command> + <arg rep=3D"repeat"><replaceable>connection-option</replaceable></arg> + <arg rep=3D"repeat"><replaceable>option</replaceable></arg> + + <arg choice=3D"plain" rep=3D"repeat"> + <arg choice=3D"opt"> + <group choice=3D"plain"> + <arg choice=3D"plain"><option>-t</option></arg> + <arg choice=3D"plain"><option>--table</option></arg> + </group> + <replaceable>table</replaceable> + <arg choice=3D"opt">( <replaceable class=3D"parameter">column</replac= eable> [,...] )</arg> + </arg> + </arg> + + <arg choice=3D"opt"> + <group choice=3D"plain"> + <arg choice=3D"plain"><replaceable>dbname</replaceable></arg> + <arg choice=3D"plain"><option>-a</option></arg> + <arg choice=3D"plain"><option>--all</option></arg> + </group> + </arg> + </cmdsynopsis> + + <cmdsynopsis> + <command>pg_repackdb</command> + <arg rep=3D"repeat"><replaceable>connection-option</replaceable></arg> + <arg rep=3D"repeat"><replaceable>option</replaceable></arg> + + <arg choice=3D"plain" rep=3D"repeat"> + <arg choice=3D"opt"> + <group choice=3D"plain"> + <arg choice=3D"plain"><option>-n</option></arg> + <arg choice=3D"plain"><option>--schema</option></arg> + </group> + <replaceable>schema</replaceable> + </arg> + </arg> + + <arg choice=3D"opt"> + <group choice=3D"plain"> + <arg choice=3D"plain"><replaceable>dbname</replaceable></arg> + <arg choice=3D"plain"><option>-a</option></arg> + <arg choice=3D"plain"><option>--all</option></arg> + </group> + </arg> + </cmdsynopsis> + + <cmdsynopsis> + <command>pg_repackdb</command> + <arg rep=3D"repeat"><replaceable>connection-option</replaceable></arg> + <arg rep=3D"repeat"><replaceable>option</replaceable></arg> + + <arg choice=3D"plain" rep=3D"repeat"> + <arg choice=3D"opt"> + <group choice=3D"plain"> + <arg choice=3D"plain"><option>-N</option></arg> + <arg choice=3D"plain"><option>--exclude-schema</option></arg> + </group> + <replaceable>schema</replaceable> + </arg> + </arg> + + <arg choice=3D"opt"> + <group choice=3D"plain"> + <arg choice=3D"plain"><replaceable>dbname</replaceable></arg> + <arg choice=3D"plain"><option>-a</option></arg> + <arg choice=3D"plain"><option>--all</option></arg> + </group> + </arg> + </cmdsynopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <application>pg_repackdb</application> is a utility for repacking a + <productname>PostgreSQL</productname> database. + <application>pg_repackdb</application> will also generate internal + statistics used by the <productname>PostgreSQL</productname> query + optimizer. + </para> + + <para> + <application>pg_repackdb</application> is a wrapper around the SQL + command <link linkend=3D"sql-repack"><command>REPACK</command></link>. = There + is no effective difference between repacking and analyzing databases via + this utility and via other methods for accessing the server. + </para> + </refsect1> + + <refsect1> + <title>Options</title> + + <para> + <application>pg_repackdb</application> accepts the following command-l= ine arguments: + <variablelist> + <varlistentry> + <term><option>-a</option></term> + <term><option>--all</option></term> + <listitem> + <para> + Repack all databases. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option><optional>-d</optional> <replaceable class=3D"paramete= r">dbname</replaceable></option></term> + <term><option><optional>--dbname=3D</optional><replaceable class=3D"= parameter">dbname</replaceable></option></term> + <listitem> + <para> + Specifies the name of the database to be repacked or analyzed, + when <option>-a</option>/<option>--all</option> is not used. If t= his + is not specified, the database name is read from the environment + variable <envar>PGDATABASE</envar>. If that is not set, the user = name + specified for the connection is used. + The <replaceable>dbname</replaceable> can be + a <link linkend=3D"libpq-connstring">connection string</link>. If= so, + connection string parameters will override any conflicting command + line options. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-e</option></term> + <term><option>--echo</option></term> + <listitem> + <para> + Echo the commands that <application>pg_repackdb</application> + generates and sends to the server. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>--index<optional>=3D<replaceable class=3D"parameter">i= ndex_name</replaceable></optional></option></term> + <listitem> + <para> + Pass the <literal>USING INDEX</literal> clause to <literal>REPACK<= /literal>, + and optionally the index name to specify. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-j <replaceable class=3D"parameter">njobs</replaceable= ></option></term> + <term><option>--jobs=3D<replaceable class=3D"parameter">njobs</repla= ceable></option></term> + <listitem> + <para> + Execute the repack or analyze commands in parallel by running + <replaceable class=3D"parameter">njobs</replaceable> + commands simultaneously. This option may reduce the processing ti= me + but it also increases the load on the database server. + </para> + <para> + <application>pg_repackdb</application> will open + <replaceable class=3D"parameter">njobs</replaceable> connections t= o the + database, so make sure your <xref linkend=3D"guc-max-connections"/> + setting is high enough to accommodate all connections. + </para> + <para> + Note that using this mode might cause deadlock failures if certain + system catalogs are processed in parallel. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-n <replaceable class=3D"parameter">schema</replaceabl= e></option></term> + <term><option>--schema=3D<replaceable class=3D"parameter">schema</re= placeable></option></term> + <listitem> + <para> + Repack or analyze all tables in + <replaceable class=3D"parameter">schema</replaceable> only. Multi= ple + schemas can be repacked by writing multiple <option>-n</option> + switches. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-N <replaceable class=3D"parameter">schema</replaceabl= e></option></term> + <term><option>--exclude-schema=3D<replaceable class=3D"parameter">sc= hema</replaceable></option></term> + <listitem> + <para> + Do not repack or analyze any tables in + <replaceable class=3D"parameter">schema</replaceable>. Multiple s= chemas + can be excluded by writing multiple <option>-N</option> switches. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-q</option></term> + <term><option>--quiet</option></term> + <listitem> + <para> + Do not display progress messages. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-t <replaceable class=3D"parameter">table</replaceable= > [ (<replaceable class=3D"parameter">column</replaceable> [,...]) ]</optio= n></term> + <term><option>--table=3D<replaceable class=3D"parameter">table</repl= aceable> [ (<replaceable class=3D"parameter">column</replaceable> [,...]) ]= </option></term> + <listitem> + <para> + Repack or analyze <replaceable class=3D"parameter">table</replacea= ble> + only. Column names can be specified only in conjunction with + the <option>--analyze</option> option. Multiple tables can be + repacked by writing multiple + <option>-t</option> switches. + </para> + <tip> + <para> + If you specify columns, you probably have to escape the parenthes= es + from the shell. (See examples below.) + </para> + </tip> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-v</option></term> + <term><option>--verbose</option></term> + <listitem> + <para> + Print detailed information during processing. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-V</option></term> + <term><option>--version</option></term> + <listitem> + <para> + Print the <application>pg_repackdb</application> version and exit. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-z</option></term> + <term><option>--analyze</option></term> + <listitem> + <para> + Also calculate statistics for use by the optimizer. If a column n= ame + list is given, only compute statistics for those columns. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-?</option></term> + <term><option>--help</option></term> + <listitem> + <para> + Show help about <application>pg_repackdb</application> command line + arguments, and exit. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + <application>pg_repackdb</application> also accepts + the following command-line arguments for connection parameters: + <variablelist> + <varlistentry> + <term><option>-h <replaceable class=3D"parameter">host</replaceable>= </option></term> + <term><option>--host=3D<replaceable class=3D"parameter">host</replac= eable></option></term> + <listitem> + <para> + Specifies the host name of the machine on which the server + is running. If the value begins with a slash, it is used + as the directory for the Unix domain socket. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-p <replaceable class=3D"parameter">port</replaceable>= </option></term> + <term><option>--port=3D<replaceable class=3D"parameter">port</replac= eable></option></term> + <listitem> + <para> + Specifies the TCP port or local Unix domain socket file + extension on which the server + is listening for connections. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-U <replaceable class=3D"parameter">username</replacea= ble></option></term> + <term><option>--username=3D<replaceable class=3D"parameter">username= </replaceable></option></term> + <listitem> + <para> + User name to connect as. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-w</option></term> + <term><option>--no-password</option></term> + <listitem> + <para> + Never issue a password prompt. If the server requires + password authentication and a password is not available by + other means such as a <filename>.pgpass</filename> file, the + connection attempt will fail. This option can be useful in + batch jobs and scripts where no user is present to enter a + password. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>-W</option></term> + <term><option>--password</option></term> + <listitem> + <para> + Force <application>pg_repackdb</application> to prompt for a + password before connecting to a database. + </para> + + <para> + This option is never essential, since + <application>pg_repackdb</application> will automatically prompt + for a password if the server demands password authentication. + However, <application>pg_repackdb</application> will waste a + connection attempt finding out that the server wants a password. + In some cases it is worth typing <option>-W</option> to avoid the = extra + connection attempt. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><option>--maintenance-db=3D<replaceable class=3D"parameter">db= name</replaceable></option></term> + <listitem> + <para> + When the <option>-a</option>/<option>--all</option> is used, conne= ct + to this database to gather the list of databases to repack. + If not specified, the <literal>postgres</literal> database will be= used, + or if that does not exist, <literal>template1</literal> will be us= ed. + This can be a <link linkend=3D"libpq-connstring">connection + string</link>. If so, connection string parameters will override = any + conflicting command line options. Also, connection string paramet= ers + other than the database name itself will be re-used when connecting + to other databases. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + + <refsect1> + <title>Environment</title> + + <variablelist> + <varlistentry> + <term><envar>PGDATABASE</envar></term> + <term><envar>PGHOST</envar></term> + <term><envar>PGPORT</envar></term> + <term><envar>PGUSER</envar></term> + + <listitem> + <para> + Default connection parameters + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><envar>PG_COLOR</envar></term> + <listitem> + <para> + Specifies whether to use color in diagnostic messages. Possible valu= es + are <literal>always</literal>, <literal>auto</literal> and + <literal>never</literal>. + </para> + </listitem> + </varlistentry> + </variablelist> + + <para> + This utility, like most other <productname>PostgreSQL</productname> uti= lities, + also uses the environment variables supported by <application>libpq</ap= plication> + (see <xref linkend=3D"libpq-envars"/>). + </para> + + </refsect1> + + + <refsect1> + <title>Diagnostics</title> + + <para> + In case of difficulty, see + <xref linkend=3D"sql-repack"/> and <xref linkend=3D"app-psql"/> for + discussions of potential problems and error messages. + The database server must be running at the + targeted host. Also, any default connection settings and environment + variables used by the <application>libpq</application> front-end + library will apply. + </para> + + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To repack the database <literal>test</literal>: +<screen> +<prompt>$ </prompt><userinput>pg_repackdb test</userinput> +</screen> + </para> + + <para> + To repack and analyze for the optimizer a database named + <literal>bigdb</literal>: +<screen> +<prompt>$ </prompt><userinput>pg_repackdb --analyze bigdb</userinput> +</screen> + </para> + + <para> + To repack a single table + <literal>foo</literal> in a database named + <literal>xyzzy</literal>, and analyze a single column + <literal>bar</literal> of the table for the optimizer: +<screen> +<prompt>$ </prompt><userinput>pg_repackdb --analyze --verbose --table=3D'f= oo(bar)' xyzzy</userinput> +</screen></para> + + <para> + To repack all tables in the <literal>foo</literal> and <literal>bar</l= iteral> schemas + in a database named <literal>xyzzy</literal>: +<screen> +<prompt>$ </prompt><userinput>pg_repackdb --schema=3D'foo' --schema=3D'bar= ' xyzzy</userinput> +</screen></para> + + + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type=3D"inline"> + <member><xref linkend=3D"sql-repack"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml new file mode 100644 index 00000000000..61d5c2cdef1 --- /dev/null +++ b/doc/src/sgml/ref/repack.sgml @@ -0,0 +1,328 @@ +<!-- +doc/src/sgml/ref/repack.sgml +PostgreSQL documentation +--> + +<refentry id=3D"sql-repack"> + <indexterm zone=3D"sql-repack"> + <primary>REPACK</primary> + </indexterm> + + <refmeta> + <refentrytitle>REPACK</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>REPACK</refname> + <refpurpose>rewrite a table to reclaim disk space</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +REPACK [ ( <replaceable class=3D"parameter">option</replaceable> [, ...] )= ] [ <replaceable class=3D"parameter">table_and_columns</replaceable> [ USI= NG INDEX [ <replaceable class=3D"parameter">index_name</replaceable> ] ] ] +REPACK [ ( <replaceable class=3D"parameter">option</replaceable> [, ...] )= ] USING INDEX + +<phrase>where <replaceable class=3D"parameter">option</replaceable> can be= one of:</phrase> + + VERBOSE [ <replaceable class=3D"parameter">boolean</replaceable> ] + ANALYZE [ <replaceable class=3D"parameter">boolean</replaceable> ] + +<phrase>and <replaceable class=3D"parameter">table_and_columns</replaceabl= e> is:</phrase> + + <replaceable class=3D"parameter">table_name</replaceable> [ ( <replace= able class=3D"parameter">column_name</replaceable> [, ...] ) ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>REPACK</command> reclaims storage occupied by dead + tuples. Unlike <command>VACUUM</command>, it does so by rewriting the + entire contents of the table specified + by <replaceable class=3D"parameter">table_name</replaceable> into a new= disk + file with no extra space (except for the space guaranteed by + the <literal>fillfactor</literal> storage parameter), allowing unused s= pace + to be returned to the operating system. + </para> + + <para> + Without + a <replaceable class=3D"parameter">table_name</replaceable>, <command>R= EPACK</command> + 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 transacti= on + block. + </para> + + <para> + If a <literal>USING INDEX</literal> clause is specified, the rows are + physically reordered based on information from an index. Please see the + notes on clustering below. + </para> + + <para> + When a table is being repacked, an <literal>ACCESS EXCLUSIVE</literal> = lock + is acquired on it. This prevents any other database operations (both re= ads + and writes) from operating on the table until the <command>REPACK</comm= and> + is finished. + </para> + + <refsect2 id=3D"sql-repack-notes-on-clustering" xreflabel=3D"Notes on Cl= ustering"> + <title>Notes on Clustering</title> + + <para> + If the <literal>USING INDEX</literal> clause is specified, the rows in + the table are physically reordered following an index: if an index name + is specified in the command, then that index is used; if no index name + is specified, then the index that has been configured as the index to + cluster on. If no index has been configured in this way, an error is + thrown. The index given in the <literal>USING INDEX</literal> clause + is configured as the index to cluster on, as well as an index given + to the <command>CLUSTER</command> command. An index can be set + manually using <command>ALTER TABLE ... CLUSTER ON</command>, and reset + with <command>ALTER TABLE ... SET WITHOUT CLUSTER</command>. + </para> + + <para> + If no table name is specified in <command>REPACK USING INDEX</command>, + all tables which have a clustering index defined and which the calling + user has privileges for are processed. + </para> + + <para> + Clustering is a one-time operation: when the table is + subsequently updated, the changes are not clustered. That is, no atte= mpt + is made to store new or updated rows according to their index order. = (If + one wishes, one can periodically recluster by issuing the command agai= n. + Also, setting the table's <literal>fillfactor</literal> storage parame= ter + to less than 100% can aid in preserving cluster ordering during update= s, + since updated rows are kept on the same page if enough space is availa= ble + there.) + </para> + + <para> + In cases where you are accessing single rows randomly within a table, = the + actual order of the data in the table is unimportant. However, if you = tend + to access some data more than others, and there is an index that groups + them together, you will benefit from using clustering. If + you are requesting a range of indexed values from a table, or a single + indexed value that has multiple rows that match, + <command>REPACK</command> will help because once the index identifies = the + table page for the first row that matches, all other rows that match a= re + probably already on the same table page, and so you save disk accesses= and + speed up the query. + </para> + + <para> + <command>REPACK</command> can re-sort the table using either an index = scan + on the specified index (if the index is a b-tree), or a sequential scan + followed by sorting. It will attempt to choose the method that will be + faster, based on planner cost parameters and available statistical + information. + </para> + + <para> + Because the planner records statistics about the ordering of tables, i= t is + advisable to + run <link linkend=3D"sql-analyze"><command>ANALYZE</command></link> on= the + newly repacked table. Otherwise, the planner might make poor choices = of + query plans. + </para> + </refsect2> + + <refsect2 id=3D"sql-repack-notes-on-resources" xreflabel=3D"Notes on Res= ources"> + <title>Notes on Resources</title> + + <para> + When an index scan or a sequential scan without sort is used, a tempor= ary + copy of the table is created that contains the table data in the index + order. Temporary copies of each index on the table are created as wel= l. + Therefore, you need free space on disk at least equal to the sum of the + table size and the index sizes. + </para> + + <para> + When a sequential scan and sort is used, a temporary sort file is also + created, so that the peak temporary space requirement is as much as do= uble + the table size, plus the index sizes. This method is often faster than + the index scan method, but if the disk space requirement is intolerabl= e, + you can disable this choice by temporarily setting + <xref linkend=3D"guc-enable-sort"/> to <literal>off</literal>. + </para> + + <para> + It is advisable to set <xref linkend=3D"guc-maintenance-work-mem"/> to= a + reasonably large value (but not more than the amount of RAM you can + dedicate to the <command>REPACK</command> operation) before repacking. + </para> + </refsect2> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class=3D"parameter">table_name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of a table. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class=3D"parameter">column_name</replaceable></term> + <listitem> + <para> + The name of a specific column to analyze. Defaults to all columns. + If a column list is specific, <literal>ANALYZE</literal> must also + be specified. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class=3D"parameter">index_name</replaceable></term> + <listitem> + <para> + The name of an index. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>VERBOSE</literal></term> + <listitem> + <para> + Prints a progress report as each table is repacked + at <literal>INFO</literal> level. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>ANALYZE</literal></term> + <term><literal>ANALYSE</literal></term> + <listitem> + <para> + Applies <xref linkend=3D"sql-analyze"/> on the table after repacking= . This is + currently only supported when a single (non-partitioned) table is sp= ecified. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class=3D"parameter">boolean</replaceable></term> + <listitem> + <para> + Specifies whether the selected option should be turned on or off. + You can write <literal>TRUE</literal>, <literal>ON</literal>, or + <literal>1</literal> to enable the option, and <literal>FALSE</liter= al>, + <literal>OFF</literal>, or <literal>0</literal> to disable it. The + <replaceable class=3D"parameter">boolean</replaceable> value can also + be omitted, in which case <literal>TRUE</literal> is assumed. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + To repack a table, one must have the <literal>MAINTAIN</literal> privi= lege + on the table. + </para> + + <para> + While <command>REPACK</command> is running, the <xref + linkend=3D"guc-search-path"/> is temporarily changed to <literal>pg_ca= talog, + pg_temp</literal>. + </para> + + <para> + Each backend running <command>REPACK</command> will report its progress + in the <structname>pg_stat_progress_repack</structname> view. See + <xref linkend=3D"repack-progress-reporting"/> for details. + </para> + + <para> + Repacking a partitioned table repacks each of its partitions. If an in= dex + is specified, each partition is repacked using the partition of that + index. <command>REPACK</command> on a partitioned table cannot be exec= uted + inside a transaction block. + </para> + + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Repack the table <literal>employees</literal>: +<programlisting> +REPACK employees; +</programlisting> + </para> + + <para> + Repack the table <literal>employees</literal> on the basis of its + index <literal>employees_ind</literal> (Since index is used here, this = is + effectively clustering): +<programlisting> +REPACK employees USING INDEX employees_ind; +</programlisting> + </para> + + <para> + Repack the table <literal>cases</literal> on physical ordering, + running an <command>ANALYZE</command> on the given columns once + repacking is done, showing informational messages: +<programlisting> +REPACK (ANALYZE, VERBOSE) cases (district, case_nr); +</programlisting> + </para> + + <para> + Repack all tables in the database on which you have + the <literal>MAINTAIN</literal> privilege: +<programlisting> +REPACK; +</programlisting> + </para> + + <para> + Repack all tables for which a clustering index has previously been + configured on which you have the <literal>MAINTAIN</literal> privilege, + showing informational messages: +<programlisting> +REPACK (VERBOSE) USING INDEX; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + There is no <command>REPACK</command> statement in the SQL standard. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type=3D"inline"> + <member><xref linkend=3D"app-pgrepackdb"/></member> + <member><xref linkend=3D"repack-progress-reporting"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml index 6d0fdd43cfb..ac5d083d468 100644 --- a/doc/src/sgml/ref/vacuum.sgml +++ b/doc/src/sgml/ref/vacuum.sgml @@ -25,7 +25,6 @@ VACUUM [ ( <replaceable class=3D"parameter">option</repla= ceable> [, ...] ) ] [ <re =20 <phrase>where <replaceable class=3D"parameter">option</replaceable> can be= one of:</phrase> =20 - FULL [ <replaceable class=3D"parameter">boolean</replaceable> ] FREEZE [ <replaceable class=3D"parameter">boolean</replaceable> ] VERBOSE [ <replaceable class=3D"parameter">boolean</replaceable> ] ANALYZE [ <replaceable class=3D"parameter">boolean</replaceable> ] @@ -39,6 +38,7 @@ VACUUM [ ( <replaceable class=3D"parameter">option</repla= ceable> [, ...] ) ] [ <re SKIP_DATABASE_STATS [ <replaceable class=3D"parameter">boolean</replac= eable> ] ONLY_DATABASE_STATS [ <replaceable class=3D"parameter">boolean</replac= eable> ] BUFFER_USAGE_LIMIT <replaceable class=3D"parameter">size</replaceable> + FULL [ <replaceable class=3D"parameter">boolean</replaceable> ] =20 <phrase>and <replaceable class=3D"parameter">table_and_columns</replaceabl= e> is:</phrase> =20 @@ -95,20 +95,6 @@ VACUUM [ ( <replaceable class=3D"parameter">option</repl= aceable> [, ...] ) ] [ <re <title>Parameters</title> =20 <variablelist> - <varlistentry> - <term><literal>FULL</literal></term> - <listitem> - <para> - Selects <quote>full</quote> vacuum, which can reclaim more - space, but takes much longer and exclusively locks the table. - This method also requires extra disk space, since it writes a - new copy of the table and doesn't release the old copy until - the operation is complete. Usually this should only be used when a - significant amount of space needs to be reclaimed from within the ta= ble. - </para> - </listitem> - </varlistentry> - <varlistentry> <term><literal>FREEZE</literal></term> <listitem> @@ -362,6 +348,23 @@ VACUUM [ ( <replaceable class=3D"parameter">option</re= placeable> [, ...] ) ] [ <re </listitem> </varlistentry> =20 + <varlistentry> + <term><literal>FULL</literal></term> + <listitem> + <para> + This option, which is deprecated, makes <command>VACUUM</command> + behave like <command>REPACK</command> without a + <literal>USING INDEX</literal> clause. + This method of compacting the table takes much longer than + <command>VACUUM</command> and exclusively locks the table. + This method also requires extra disk space, since it writes a + new copy of the table and doesn't release the old copy until + the operation is complete. Usually this should only be used when a + significant amount of space needs to be reclaimed from within the ta= ble. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><replaceable class=3D"parameter">boolean</replaceable></term> <listitem> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 2cf02c37b17..5d9a8a25a02 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -195,6 +195,7 @@ &refreshMaterializedView; &reindex; &releaseSavepoint; + &repack; &reset; &revoke; &rollback; @@ -258,6 +259,7 @@ &pgIsready; &pgReceivewal; &pgRecvlogical; + &pgRepackdb; &pgRestore; &pgVerifyBackup; &psqlRef; diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/= heap/heapam_handler.c index cbef73e5d4b..7d4b48e5a97 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -741,13 +741,13 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Re= lation NewHeap, if (OldIndex !=3D NULL && !use_sort) { const int ci_index[] =3D { - PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_INDEX_RELID + PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_INDEX_RELID }; int64 ci_val[2]; =20 /* Set phase and OIDOldIndex to columns */ - ci_val[0] =3D PROGRESS_CLUSTER_PHASE_INDEX_SCAN_HEAP; + ci_val[0] =3D PROGRESS_REPACK_PHASE_INDEX_SCAN_HEAP; ci_val[1] =3D RelationGetRelid(OldIndex); pgstat_progress_update_multi_param(2, ci_index, ci_val); =20 @@ -759,15 +759,15 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Re= lation NewHeap, else { /* In scan-and-sort mode and also VACUUM FULL, set phase */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_SEQ_SCAN_HEAP); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP); =20 tableScan =3D table_beginscan(OldHeap, SnapshotAny, 0, (ScanKey) NULL); heapScan =3D (HeapScanDesc) tableScan; indexScan =3D NULL; =20 /* Set total heap blocks */ - pgstat_progress_update_param(PROGRESS_CLUSTER_TOTAL_HEAP_BLKS, + pgstat_progress_update_param(PROGRESS_REPACK_TOTAL_HEAP_BLKS, heapScan->rs_nblocks); } =20 @@ -809,7 +809,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Rela= tion NewHeap, * is manually updated to the correct value when the table * scan finishes. */ - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_BLKS_SCANNED, heapScan->rs_nblocks); break; } @@ -825,7 +825,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Rela= tion NewHeap, */ if (prev_cblock !=3D heapScan->rs_cblock) { - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_BLKS_SCANNED, (heapScan->rs_cblock + heapScan->rs_nblocks - heapScan->rs_startblock @@ -926,14 +926,14 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Re= lation NewHeap, * In scan-and-sort mode, report increase in number of tuples * scanned */ - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_SCANNED, *num_tuples); } else { const int ct_index[] =3D { - PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED, - PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN + PROGRESS_REPACK_HEAP_TUPLES_SCANNED, + PROGRESS_REPACK_HEAP_TUPLES_WRITTEN }; int64 ct_val[2]; =20 @@ -966,14 +966,14 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Re= lation NewHeap, double n_tuples =3D 0; =20 /* Report that we are now sorting tuples */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_SORT_TUPLES); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_SORT_TUPLES); =20 tuplesort_performsort(tuplesort); =20 /* Report that we are now writing new heap */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_WRITE_NEW_HEAP); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); =20 for (;;) { @@ -991,7 +991,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Rela= tion NewHeap, values, isnull, rwstate); /* Report n_tuples */ - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_WRITTEN, n_tuples); } =20 diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 43de42ce39e..5ee6389d39c 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -4077,7 +4077,7 @@ reindex_relation(const ReindexStmt *stmt, Oid relid, = int flags, Assert(!ReindexIsProcessingIndex(indexOid)); =20 /* Set index rebuild count */ - pgstat_progress_update_param(PROGRESS_CLUSTER_INDEX_REBUILD_COUNT, + pgstat_progress_update_param(PROGRESS_REPACK_INDEX_REBUILD_COUNT, i); i++; } diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/sys= tem_views.sql index 7553f31fef0..3f05ba3083a 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1283,14 +1283,15 @@ CREATE VIEW pg_stat_progress_vacuum AS FROM pg_stat_get_progress_info('VACUUM') AS S LEFT JOIN pg_database D ON S.datid =3D D.oid; =20 -CREATE VIEW pg_stat_progress_cluster AS +CREATE VIEW pg_stat_progress_repack AS SELECT S.pid AS pid, S.datid AS datid, D.datname AS datname, S.relid AS relid, CASE S.param1 WHEN 1 THEN 'CLUSTER' - WHEN 2 THEN 'VACUUM FULL' + WHEN 2 THEN 'REPACK' + WHEN 3 THEN 'VACUUM FULL' END AS command, CASE S.param2 WHEN 0 THEN 'initializing' WHEN 1 THEN 'seq scanning heap' @@ -1301,15 +1302,35 @@ CREATE VIEW pg_stat_progress_cluster AS WHEN 6 THEN 'rebuilding index' WHEN 7 THEN 'performing final cleanup' END AS phase, - CAST(S.param3 AS oid) AS cluster_index_relid, + CAST(S.param3 AS oid) AS repack_index_relid, S.param4 AS heap_tuples_scanned, S.param5 AS heap_tuples_written, S.param6 AS heap_blks_total, S.param7 AS heap_blks_scanned, S.param8 AS index_rebuild_count - FROM pg_stat_get_progress_info('CLUSTER') AS S + FROM pg_stat_get_progress_info('REPACK') AS S LEFT JOIN pg_database D ON S.datid =3D D.oid; =20 +-- This view is as the one above, except for renaming a column and avoiding +-- 'REPACK' as a command name to report. +CREATE VIEW pg_stat_progress_cluster AS + SELECT + pid, + datid, + datname, + relid, + CASE WHEN command IN ('CLUSTER', 'VACUUM FULL') THEN command + WHEN repack_index_relid =3D 0 THEN 'VACUUM FULL' + ELSE 'CLUSTER' END AS command, + phase, + repack_index_relid AS cluster_index_relid, + heap_tuples_scanned, + heap_tuples_written, + heap_blks_total, + heap_blks_scanned, + index_rebuild_count + FROM pg_stat_progress_repack; + CREATE VIEW pg_stat_progress_create_index AS SELECT S.pid AS pid, S.datid AS datid, D.datname AS datname, diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index 60a4617a585..e19675a6d05 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -1,7 +1,8 @@ /*------------------------------------------------------------------------- * * cluster.c - * CLUSTER a table on an index. This is now also used for VACUUM FULL. + * CLUSTER a table on an index. This is now also used for VACUUM FULL a= nd + * REPACK. * * There is hardly anything left of Paul Brown's original implementation... * @@ -67,27 +68,35 @@ typedef struct Oid indexOid; } RelToCluster; =20 - -static void cluster_multiple_rels(List *rtcs, ClusterParams *params); +static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, + Oid indexOid, Oid userid, int options); static void rebuild_relation(Relation OldHeap, Relation index, bool verbos= e); static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation O= ldIndex, bool verbose, bool *pSwapToastByContent, TransactionId *pFreezeXid, MultiXactId *pCutoffMulti); -static List *get_tables_to_cluster(MemoryContext cluster_context); -static List *get_tables_to_cluster_partitioned(MemoryContext cluster_conte= xt, - Oid indexOid); -static bool cluster_is_permitted_for_relation(Oid relid, Oid userid); +static List *get_tables_to_repack(RepackCommand cmd, bool usingindex, + MemoryContext permcxt); +static List *get_tables_to_repack_partitioned(RepackCommand cmd, + Oid relid, bool rel_is_index, + MemoryContext permcxt); +static bool cluster_is_permitted_for_relation(RepackCommand cmd, + Oid relid, Oid userid); +static Relation process_single_relation(RepackStmt *stmt, + ClusterParams *params); +static Oid determine_clustered_index(Relation rel, bool usingindex, + const char *indexname); +static const char *RepackCommandAsString(RepackCommand cmd); =20 =20 -/*------------------------------------------------------------------------= --- - * This cluster code allows for clustering multiple tables at once. Because +/* + * The repack code allows for processing multiple tables at once. Because * of this, we cannot just run everything on a single transaction, or we * would be forced to acquire exclusive locks on all the tables being * clustered, simultaneously --- very likely leading to deadlock. * - * To solve this we follow a similar strategy to VACUUM code, - * clustering each relation in a separate transaction. For this to work, - * we need to: + * To solve this we follow a similar strategy to VACUUM code, processing e= ach + * relation in a separate transaction. For this to work, we need to: + * * - provide a separate memory context so that we can pass information in * a way that survives across transactions * - start a new transaction every time a new relation is clustered @@ -98,197 +107,166 @@ static bool cluster_is_permitted_for_relation(Oid re= lid, Oid userid); * * The single-relation case does not have any such overhead. * - * We also allow a relation to be specified without index. In that case, - * the indisclustered bit will be looked up, and an ERROR will be thrown - * if there is no index with the bit set. - *------------------------------------------------------------------------= --- + * We also allow a relation to be repacked following an index, but without + * naming a specific one. In that case, the indisclustered bit will be + * looked up, and an ERROR will be thrown if no so-marked index is found. */ void -cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel) +ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) { - ListCell *lc; ClusterParams params =3D {0}; - bool verbose =3D false; Relation rel =3D NULL; - Oid indexOid =3D InvalidOid; - MemoryContext cluster_context; + MemoryContext repack_context; List *rtcs; =20 /* Parse option list */ - foreach(lc, stmt->params) + foreach_node(DefElem, opt, stmt->params) { - DefElem *opt =3D (DefElem *) lfirst(lc); - if (strcmp(opt->defname, "verbose") =3D=3D 0) - verbose =3D defGetBoolean(opt); + params.options |=3D defGetBoolean(opt) ? CLUOPT_VERBOSE : 0; + else if (strcmp(opt->defname, "analyze") =3D=3D 0 || + strcmp(opt->defname, "analyse") =3D=3D 0) + params.options |=3D defGetBoolean(opt) ? CLUOPT_ANALYZE : 0; else ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("unrecognized %s option \"%s\"", - "CLUSTER", opt->defname), - parser_errposition(pstate, opt->location))); + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unrecognized %s option \"%s\"", + RepackCommandAsString(stmt->command), + opt->defname), + parser_errposition(pstate, opt->location)); } =20 - params.options =3D (verbose ? CLUOPT_VERBOSE : 0); - + /* + * If a single relation is specified, process it and we're done ... unless + * the relation is a partitioned table, in which case we fall through. + */ if (stmt->relation !=3D NULL) { - /* This is the single-relation case. */ - Oid tableOid; - - /* - * Find, lock, and check permissions on the table. We obtain - * AccessExclusiveLock right away to avoid lock-upgrade hazard in the - * single-transaction case. - */ - tableOid =3D RangeVarGetRelidExtended(stmt->relation, - AccessExclusiveLock, - 0, - RangeVarCallbackMaintainsTable, - NULL); - rel =3D table_open(tableOid, NoLock); - - /* - * Reject clustering a remote temp table ... their local buffer - * manager is not going to cope. - */ - if (RELATION_IS_OTHER_TEMP(rel)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster temporary tables of other sessions"))); - - if (stmt->indexname =3D=3D NULL) - { - ListCell *index; - - /* We need to find the index that has indisclustered set. */ - foreach(index, RelationGetIndexList(rel)) - { - indexOid =3D lfirst_oid(index); - if (get_index_isclustered(indexOid)) - break; - indexOid =3D InvalidOid; - } - - if (!OidIsValid(indexOid)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("there is no previously clustered index for table \"%s\"", - stmt->relation->relname))); - } - else - { - /* - * The index is expected to be in the same namespace as the - * relation. - */ - indexOid =3D get_relname_relid(stmt->indexname, - rel->rd_rel->relnamespace); - if (!OidIsValid(indexOid)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("index \"%s\" for table \"%s\" does not exist", - stmt->indexname, stmt->relation->relname))); - } - - /* For non-partitioned tables, do what we came here to do. */ - if (rel->rd_rel->relkind !=3D RELKIND_PARTITIONED_TABLE) - { - cluster_rel(rel, indexOid, ¶ms); - /* cluster_rel closes the relation, but keeps lock */ - - return; - } + rel =3D process_single_relation(stmt, ¶ms); + if (rel =3D=3D NULL) + return; /* all done */ } =20 + /* + * Don't allow ANALYZE in the multiple-relation case for now. Maybe we + * can add support for this later. + */ + if (params.options & CLUOPT_ANALYZE) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + 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. */ - PreventInTransactionBlock(isTopLevel, "CLUSTER"); + PreventInTransactionBlock(isTopLevel, RepackCommandAsString(stmt->command= )); =20 /* Also, we need a memory context to hold our list of relations */ - cluster_context =3D AllocSetContextCreate(PortalContext, - "Cluster", - ALLOCSET_DEFAULT_SIZES); + repack_context =3D AllocSetContextCreate(PortalContext, + "Repack", + ALLOCSET_DEFAULT_SIZES); + + params.options |=3D CLUOPT_RECHECK; =20 /* - * Either we're processing a partitioned table, or we were not given any - * table name at all. In either case, obtain a list of relations to - * process. - * - * In the former case, an index name must have been given, so we don't - * need to recheck its "indisclustered" bit, but we have to check that it - * is an index that we can cluster on. In the latter case, we set the - * option bit to have indisclustered verified. - * - * Rechecking the relation itself is necessary here in all cases. + * If we don't have a relation yet, determine a relation list. If we do, + * then it must be a partitioned table, and we want to process its + * partitions. */ - params.options |=3D CLUOPT_RECHECK; - if (rel !=3D NULL) + if (rel =3D=3D NULL) { - Assert(rel->rd_rel->relkind =3D=3D RELKIND_PARTITIONED_TABLE); - check_index_is_clusterable(rel, indexOid, AccessShareLock); - rtcs =3D get_tables_to_cluster_partitioned(cluster_context, indexOid); - - /* close relation, releasing lock on parent table */ - table_close(rel, AccessExclusiveLock); + Assert(stmt->indexname =3D=3D NULL); + rtcs =3D get_tables_to_repack(stmt->command, stmt->usingindex, + repack_context); + params.options |=3D CLUOPT_RECHECK_ISCLUSTERED; } else { - rtcs =3D get_tables_to_cluster(cluster_context); - params.options |=3D CLUOPT_RECHECK_ISCLUSTERED; - } + Oid relid; + bool rel_is_index; =20 - /* Do the job. */ - cluster_multiple_rels(rtcs, ¶ms); + Assert(rel->rd_rel->relkind =3D=3D RELKIND_PARTITIONED_TABLE); =20 - /* Start a new transaction for the cleanup work. */ - StartTransactionCommand(); + /* + * If USING INDEX was specified, resolve the index name now and pass + * it down. + */ + if (stmt->usingindex) + { + /* + * If no index name was specified when repacking a partitioned + * table, punt for now. Maybe we can improve this later. + */ + if (!stmt->indexname) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("there is no previously clustered index for table \"%s\"", + RelationGetRelationName(rel))); + + relid =3D determine_clustered_index(rel, stmt->usingindex, + stmt->indexname); + if (!OidIsValid(relid)) + elog(ERROR, "unable to determine index to cluster on"); + /* XXX is this the right place for this check? */ + check_index_is_clusterable(rel, relid, AccessExclusiveLock); + rel_is_index =3D true; + } + else + { + relid =3D RelationGetRelid(rel); + rel_is_index =3D false; + } =20 - /* Clean up working storage */ - MemoryContextDelete(cluster_context); -} + rtcs =3D get_tables_to_repack_partitioned(stmt->command, + relid, rel_is_index, + repack_context); =20 -/* - * Given a list of relations to cluster, process each of them in a separate - * transaction. - * - * We expect to be in a transaction at start, but there isn't one when we - * return. - */ -static void -cluster_multiple_rels(List *rtcs, ClusterParams *params) -{ - ListCell *lc; + /* close parent relation, releasing lock on it */ + table_close(rel, AccessExclusiveLock); + rel =3D NULL; + } =20 /* Commit to get out of starting transaction */ PopActiveSnapshot(); CommitTransactionCommand(); =20 /* Cluster the tables, each in a separate transaction */ - foreach(lc, rtcs) + Assert(rel =3D=3D NULL); + foreach_ptr(RelToCluster, rtc, rtcs) { - RelToCluster *rtc =3D (RelToCluster *) lfirst(lc); - Relation rel; - /* Start a new transaction for each relation. */ StartTransactionCommand(); =20 + /* + * Open the target table, coping with the case where it has been + * dropped. + */ + rel =3D try_table_open(rtc->tableOid, AccessExclusiveLock); + if (rel =3D=3D NULL) + { + CommitTransactionCommand(); + continue; + } + /* functions in indexes may want a snapshot set */ PushActiveSnapshot(GetTransactionSnapshot()); =20 - rel =3D table_open(rtc->tableOid, AccessExclusiveLock); - /* Process this table */ - cluster_rel(rel, rtc->indexOid, params); + cluster_rel(stmt->command, rel, rtc->indexOid, ¶ms); /* cluster_rel closes the relation, but keeps lock */ =20 PopActiveSnapshot(); CommitTransactionCommand(); } + + /* Start a new transaction for the cleanup work. */ + StartTransactionCommand(); + + /* Clean up working storage */ + MemoryContextDelete(repack_context); } =20 /* @@ -304,11 +282,14 @@ cluster_multiple_rels(List *rtcs, ClusterParams *para= ms) * them incrementally while we load the table. * * If indexOid is InvalidOid, the table will be rewritten in physical order - * instead of index order. This is the new implementation of VACUUM FULL, - * and error messages should refer to the operation as VACUUM not CLUSTER. + * instead of index order. + * + * 'cmd' indicates which command is being executed, to be used for error + * messages. */ void -cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params) +cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, + ClusterParams *params) { Oid tableOid =3D RelationGetRelid(OldHeap); Oid save_userid; @@ -323,13 +304,8 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterPar= ams *params) /* Check for user-requested abort. */ CHECK_FOR_INTERRUPTS(); =20 - pgstat_progress_start_command(PROGRESS_COMMAND_CLUSTER, tableOid); - if (OidIsValid(indexOid)) - pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND, - PROGRESS_CLUSTER_COMMAND_CLUSTER); - else - pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND, - PROGRESS_CLUSTER_COMMAND_VACUUM_FULL); + pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid); + pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd); =20 /* * Switch to the table owner's userid, so that any index functions are run @@ -350,86 +326,38 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterPa= rams *params) * *must* skip the one on indisclustered since it would reject an attempt * to cluster a not-previously-clustered index. */ - if (recheck) - { - /* Check that the user still has privileges for the relation */ - if (!cluster_is_permitted_for_relation(tableOid, save_userid)) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - - /* - * Silently skip a temp table for a remote session. Only doing this - * check in the "recheck" case is appropriate (which currently means - * somebody is executing a database-wide CLUSTER or on a partitioned - * table), because there is another check in cluster() which will stop - * any attempt to cluster remote temp tables by name. There is - * another check in cluster_rel which is redundant, but we leave it - * for extra safety. - */ - if (RELATION_IS_OTHER_TEMP(OldHeap)) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - - if (OidIsValid(indexOid)) - { - /* - * Check that the index still exists - */ - if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid))) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - - /* - * Check that the index is still the one with indisclustered set, - * if needed. - */ - if ((params->options & CLUOPT_RECHECK_ISCLUSTERED) !=3D 0 && - !get_index_isclustered(indexOid)) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - } - } + if (recheck && + !cluster_rel_recheck(cmd, OldHeap, indexOid, save_userid, + params->options)) + goto out; =20 /* - * We allow VACUUM FULL, but not CLUSTER, on shared catalogs. CLUSTER - * would work in most respects, but the index would only get marked as - * indisclustered in the current database, leading to unexpected behavior - * if CLUSTER were later invoked in another database. + * We allow repacking shared catalogs only when not using an index. It + * would work to use an index in most respects, but the index would only + * get marked as indisclustered in the current database, leading to + * unexpected behavior if CLUSTER were later invoked in another database. */ if (OidIsValid(indexOid) && OldHeap->rd_rel->relisshared) ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster a shared catalog"))); + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot run %s on a shared catalog", + RepackCommandAsString(cmd))); =20 /* * Don't process temp tables of other backends ... their local buffer * manager is not going to cope. */ if (RELATION_IS_OTHER_TEMP(OldHeap)) - { - if (OidIsValid(indexOid)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster temporary tables of other sessions"))); - else - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot vacuum temporary tables of other sessions"))); - } + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot run %s on temporary tables of other sessions", + RepackCommandAsString(cmd))); =20 /* * Also check for active uses of the relation in the current transaction, * including open scans and pending AFTER trigger events. */ - CheckTableNotInUse(OldHeap, OidIsValid(indexOid) ? "CLUSTER" : "VACUUM"); + CheckTableNotInUse(OldHeap, RepackCommandAsString(cmd)); =20 /* Check heap and index are valid to cluster on */ if (OidIsValid(indexOid)) @@ -442,6 +370,24 @@ cluster_rel(Relation OldHeap, Oid indexOid, ClusterPar= ams *params) else index =3D NULL; =20 + /* + * When allow_system_table_mods is turned off, we disallow repacking a + * catalog on a particular index unless that's already the clustered index + * for that catalog. + * + * XXX We don't check for this in CLUSTER, because it's historically been + * allowed. + */ + if (cmd !=3D REPACK_COMMAND_CLUSTER && + !allowSystemTableMods && OidIsValid(indexOid) && + IsCatalogRelation(OldHeap) && !index->rd_index->indisclustered) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: \"%s\" is a system catalog", + RelationGetRelationName(OldHeap)), + errdetail("System catalogs can only be clustered by the index they're = already clustered on, if any, unless \"%s\" is enabled.", + "allow_system_table_mods")); + /* * Quietly ignore the request if this is a materialized view which has not * been populated from its query. No harm is done because there is no data @@ -482,6 +428,63 @@ out: pgstat_progress_end_command(); } =20 +/* + * Check if the table (and its index) still meets the requirements of + * cluster_rel(). + */ +static bool +cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, + Oid userid, int options) +{ + Oid tableOid =3D 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); + return false; + } + + /* + * Silently skip a temp table for a remote session. Only doing this check + * in the "recheck" case is appropriate (which currently means somebody is + * executing a database-wide CLUSTER or on a partitioned table), because + * there is another check in cluster() which will stop any attempt to + * cluster remote temp tables by name. There is another check in + * cluster_rel which is redundant, but we leave it for extra safety. + */ + if (RELATION_IS_OTHER_TEMP(OldHeap)) + { + relation_close(OldHeap, AccessExclusiveLock); + return false; + } + + if (OidIsValid(indexOid)) + { + /* + * Check that the index still exists + */ + if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid))) + { + relation_close(OldHeap, AccessExclusiveLock); + return false; + } + + /* + * Check that the index is still the one with indisclustered set, if + * needed. + */ + if ((options & CLUOPT_RECHECK_ISCLUSTERED) !=3D 0 && + !get_index_isclustered(indexOid)) + { + relation_close(OldHeap, AccessExclusiveLock); + return false; + } + } + + return true; +} + /* * Verify that the specified heap and index are valid to cluster on * @@ -642,8 +645,8 @@ rebuild_relation(Relation OldHeap, Relation index, bool= verbose) Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false) && (index =3D=3D NULL || CheckRelationLockedByMe(index, AccessExclusiveL= ock, false))); =20 - if (index) - /* Mark the correct index as clustered */ + /* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */ + if (index !=3D NULL) mark_index_clustered(OldHeap, RelationGetRelid(index), true); =20 /* Remember info about rel before closing OldHeap */ @@ -958,20 +961,20 @@ copy_table_data(Relation NewHeap, Relation OldHeap, R= elation OldIndex, bool verb /* Log what we're doing */ if (OldIndex !=3D NULL && !use_sort) ereport(elevel, - (errmsg("clustering \"%s.%s\" using index scan on \"%s\"", - nspname, - RelationGetRelationName(OldHeap), - RelationGetRelationName(OldIndex)))); + errmsg("repacking \"%s.%s\" using index scan on \"%s\"", + nspname, + RelationGetRelationName(OldHeap), + RelationGetRelationName(OldIndex))); else if (use_sort) ereport(elevel, - (errmsg("clustering \"%s.%s\" using sequential scan and sort", - nspname, - RelationGetRelationName(OldHeap)))); + errmsg("repacking \"%s.%s\" using sequential scan and sort", + nspname, + RelationGetRelationName(OldHeap))); else ereport(elevel, - (errmsg("vacuuming \"%s.%s\"", - nspname, - RelationGetRelationName(OldHeap)))); + errmsg("repacking \"%s.%s\" in physical order", + nspname, + RelationGetRelationName(OldHeap))); =20 /* * Hand off the actual copying to AM specific function, the generic code @@ -1458,8 +1461,8 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, int i; =20 /* Report that we are now swapping relation files */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_SWAP_REL_FILES); =20 /* Zero out possible results from swapped_relation_files */ memset(mapped_tables, 0, sizeof(mapped_tables)); @@ -1509,14 +1512,14 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, reindex_flags |=3D REINDEX_REL_FORCE_INDEXES_PERMANENT; =20 /* Report that we are now reindexing relations */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_REBUILD_INDEX); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_REBUILD_INDEX); =20 reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params); =20 /* Report that we are now doing clean up */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_FINAL_CLEANUP); =20 /* * If the relation being rebuilt is pg_class, swap_relation_files() @@ -1632,106 +1635,191 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, } } =20 - /* - * Get a list of tables that the current user has privileges on and - * have indisclustered set. Return the list in a List * of RelToCluster - * (stored in the specified memory context), each one giving the tableOid - * and the indexOid on which the table is already clustered. + * Determine which relations to process, when REPACK/CLUSTER is called + * without specifying a table name. The exact process depends on whether + * USING INDEX was given or not, and in any case we only return tables and + * materialized views that the current user has privileges to repack/clust= er. + * + * If USING INDEX was given, we scan pg_index to find those that have + * indisclustered set; if it was not given, scan pg_class and return all + * tables. + * + * Return it as a list of RelToCluster in the given memory context. */ static List * -get_tables_to_cluster(MemoryContext cluster_context) +get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext per= mcxt) { - Relation indRelation; + Relation catalog; TableScanDesc scan; - ScanKeyData entry; - HeapTuple indexTuple; - Form_pg_index index; - MemoryContext old_context; + HeapTuple tuple; List *rtcs =3D NIL; =20 - /* - * Get all indexes that have indisclustered set and that the current user - * has the appropriate privileges for. - */ - indRelation =3D table_open(IndexRelationId, AccessShareLock); - ScanKeyInit(&entry, - Anum_pg_index_indisclustered, - BTEqualStrategyNumber, F_BOOLEQ, - BoolGetDatum(true)); - scan =3D table_beginscan_catalog(indRelation, 1, &entry); - while ((indexTuple =3D heap_getnext(scan, ForwardScanDirection)) !=3D NUL= L) + if (usingindex) { - RelToCluster *rtc; + ScanKeyData entry; + + catalog =3D table_open(IndexRelationId, AccessShareLock); + ScanKeyInit(&entry, + Anum_pg_index_indisclustered, + BTEqualStrategyNumber, F_BOOLEQ, + BoolGetDatum(true)); + scan =3D table_beginscan_catalog(catalog, 1, &entry); + while ((tuple =3D heap_getnext(scan, ForwardScanDirection)) !=3D NULL) + { + RelToCluster *rtc; + Form_pg_index index; + MemoryContext oldcxt; =20 - index =3D (Form_pg_index) GETSTRUCT(indexTuple); + index =3D (Form_pg_index) GETSTRUCT(tuple); =20 - if (!cluster_is_permitted_for_relation(index->indrelid, GetUserId())) - continue; + /* + * Try to obtain a light lock on the index's table, to ensure it + * doesn't go away while we collect the list. If we cannot, just + * disregard it. + */ + if (!ConditionalLockRelationOid(index->indrelid, AccessShareLock)) + continue; =20 - /* Use a permanent memory context for the result list */ - old_context =3D MemoryContextSwitchTo(cluster_context); + /* Verify that the table still exists */ + if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(index->indrelid))) + { + /* Release useless lock */ + UnlockRelationOid(index->indrelid, AccessShareLock); + continue; + } =20 - rtc =3D palloc_object(RelToCluster); - rtc->tableOid =3D index->indrelid; - rtc->indexOid =3D index->indexrelid; - rtcs =3D lappend(rtcs, rtc); + if (!cluster_is_permitted_for_relation(cmd, index->indrelid, + GetUserId())) + continue; + + /* Use a permanent memory context for the result list */ + oldcxt =3D MemoryContextSwitchTo(permcxt); + rtc =3D palloc_object(RelToCluster); + rtc->tableOid =3D index->indrelid; + rtc->indexOid =3D index->indexrelid; + rtcs =3D lappend(rtcs, rtc); + MemoryContextSwitchTo(oldcxt); + } + } + else + { + catalog =3D table_open(RelationRelationId, AccessShareLock); + scan =3D table_beginscan_catalog(catalog, 0, NULL); + + while ((tuple =3D heap_getnext(scan, ForwardScanDirection)) !=3D NULL) + { + RelToCluster *rtc; + Form_pg_class class; + MemoryContext oldcxt; + + class =3D (Form_pg_class) GETSTRUCT(tuple); + + /* + * Try to obtain a light lock on the table, to ensure it doesn't + * go away while we collect the list. If we cannot, just + * disregard the table. + */ + if (!ConditionalLockRelationOid(class->oid, AccessShareLock)) + continue; =20 - MemoryContextSwitchTo(old_context); + /* Verify that the table still exists */ + if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(class->oid))) + { + /* Release useless lock */ + UnlockRelationOid(class->oid, AccessShareLock); + continue; + } + + /* Can only process plain tables and matviews */ + if (class->relkind !=3D RELKIND_RELATION && + class->relkind !=3D RELKIND_MATVIEW) + continue; + + /* noisily skip rels which the user can't process */ + if (!cluster_is_permitted_for_relation(cmd, class->oid, + GetUserId())) + continue; + + /* Use a permanent memory context for the result list */ + oldcxt =3D MemoryContextSwitchTo(permcxt); + rtc =3D palloc_object(RelToCluster); + rtc->tableOid =3D class->oid; + rtc->indexOid =3D InvalidOid; + rtcs =3D lappend(rtcs, rtc); + MemoryContextSwitchTo(oldcxt); + } } - table_endscan(scan); =20 - relation_close(indRelation, AccessShareLock); + table_endscan(scan); + relation_close(catalog, AccessShareLock); =20 return rtcs; } =20 /* - * Given an index on a partitioned table, return a list of RelToCluster for + * Given a partitioned table or its index, return a list of RelToCluster f= or * all the children leaves tables/indexes. * * Like expand_vacuum_rel, but here caller must hold AccessExclusiveLock * on the table containing the index. + * + * 'rel_is_index' tells whether 'relid' is that of an index (true) or of t= he + * owning relation. */ static List * -get_tables_to_cluster_partitioned(MemoryContext cluster_context, Oid index= Oid) +get_tables_to_repack_partitioned(RepackCommand cmd, Oid relid, + bool rel_is_index, MemoryContext permcxt) { List *inhoids; - ListCell *lc; List *rtcs =3D NIL; - MemoryContext old_context; - - /* Do not lock the children until they're processed */ - inhoids =3D find_all_inheritors(indexOid, NoLock, NULL); =20 - foreach(lc, inhoids) + /* + * Do not lock the children until they're processed. Note that we do hold + * a lock on the parent partitioned table. + */ + inhoids =3D find_all_inheritors(relid, NoLock, NULL); + foreach_oid(child_oid, inhoids) { - Oid indexrelid =3D lfirst_oid(lc); - Oid relid =3D IndexGetRelation(indexrelid, false); + Oid table_oid, + index_oid; RelToCluster *rtc; + MemoryContext oldcxt; =20 - /* consider only leaf indexes */ - if (get_rel_relkind(indexrelid) !=3D RELKIND_INDEX) - continue; + if (rel_is_index) + { + /* consider only leaf indexes */ + if (get_rel_relkind(child_oid) !=3D RELKIND_INDEX) + continue; + + table_oid =3D IndexGetRelation(child_oid, false); + index_oid =3D child_oid; + } + else + { + /* consider only leaf relations */ + if (get_rel_relkind(child_oid) !=3D RELKIND_RELATION) + continue; + + table_oid =3D child_oid; + index_oid =3D InvalidOid; + } =20 /* * It's possible that the user does not have privileges to CLUSTER the - * leaf partition despite having such privileges on the partitioned - * table. We skip any partitions which the user is not permitted to - * CLUSTER. + * leaf partition despite having them on the partitioned table. Skip + * if so. */ - if (!cluster_is_permitted_for_relation(relid, GetUserId())) + if (!cluster_is_permitted_for_relation(cmd, table_oid, GetUserId())) continue; =20 /* Use a permanent memory context for the result list */ - old_context =3D MemoryContextSwitchTo(cluster_context); - + oldcxt =3D MemoryContextSwitchTo(permcxt); rtc =3D palloc_object(RelToCluster); - rtc->tableOid =3D relid; - rtc->indexOid =3D indexrelid; + rtc->tableOid =3D table_oid; + rtc->indexOid =3D index_oid; rtcs =3D lappend(rtcs, rtc); - - MemoryContextSwitchTo(old_context); + MemoryContextSwitchTo(oldcxt); } =20 return rtcs; @@ -1742,13 +1830,167 @@ get_tables_to_cluster_partitioned(MemoryContext cl= uster_context, Oid indexOid) * function emits a WARNING. */ static bool -cluster_is_permitted_for_relation(Oid relid, Oid userid) +cluster_is_permitted_for_relation(RepackCommand cmd, Oid relid, Oid userid) { + Assert(cmd =3D=3D REPACK_COMMAND_CLUSTER || cmd =3D=3D REPACK_COMMAND_REP= ACK); + if (pg_class_aclcheck(relid, userid, ACL_MAINTAIN) =3D=3D ACLCHECK_OK) return true; =20 ereport(WARNING, - (errmsg("permission denied to cluster \"%s\", skipping it", - get_rel_name(relid)))); + errmsg("permission denied to execute %s on \"%s\", skipping it", + RepackCommandAsString(cmd), + get_rel_name(relid))); + return false; } + + +/* + * Given a RepackStmt with an indicated relation name, resolve the relation + * name, obtain lock on it, then determine what to do based on the relation + * type: if it's table and not partitioned, repack it as indicated (using = an + * existing clustered index, or following the given one), and return NULL. + * + * On the other hand, if the table is partitioned, do nothing further and + * instead return the opened and locked relcache entry, so that caller can + * process the partitions using the multiple-table handling code. In this + * 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) +{ + Relation rel; + Oid tableOid; + + Assert(stmt->relation !=3D NULL); + Assert(stmt->command =3D=3D REPACK_COMMAND_CLUSTER || + stmt->command =3D=3D 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. + */ + tableOid =3D RangeVarGetRelidExtended(stmt->relation->relation, + AccessExclusiveLock, + 0, + RangeVarCallbackMaintainsTable, + NULL); + rel =3D table_open(tableOid, NoLock); + + /* + * Reject clustering a remote temp table ... their local buffer manager is + * not going to cope. + */ + if (RELATION_IS_OTHER_TEMP(rel)) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot execute %s on temporary tables of other sessions", + RepackCommandAsString(stmt->command))); + + /* + * Make sure ANALYZE is specified if a column list is present. + */ + if ((params->options & CLUOPT_ANALYZE) =3D=3D 0 && stmt->relation->va_col= s !=3D NIL) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("ANALYZE option must be specified when a column list is provide= d")); + + /* + * For partitioned tables, let caller handle this. Otherwise, process it + * here and we're done. + */ + if (rel->rd_rel->relkind =3D=3D RELKIND_PARTITIONED_TABLE) + return rel; + else + { + Oid indexOid; + + indexOid =3D 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); + + /* Do an analyze, if requested */ + if (params->options & CLUOPT_ANALYZE) + { + VacuumParams vac_params =3D {0}; + + vac_params.options |=3D VACOPT_ANALYZE; + if (params->options & CLUOPT_VERBOSE) + vac_params.options |=3D VACOPT_VERBOSE; + analyze_rel(tableOid, NULL, vac_params, + stmt->relation->va_cols, true, NULL); + } + + return NULL; + } +} + +/* + * Given a relation and the usingindex/indexname options in a + * REPACK USING INDEX or CLUSTER command, return the OID of the + * index to use for clustering the table. + * + * Caller must hold lock on the relation so that the set of indexes + * doesn't change, and must call check_index_is_clusterable. + */ +static Oid +determine_clustered_index(Relation rel, bool usingindex, const char *index= name) +{ + Oid indexOid; + + if (indexname =3D=3D NULL && usingindex) + { + /* + * If USING INDEX with no name is given, find a clustered index, or + * error out if none. + */ + indexOid =3D InvalidOid; + foreach_oid(idxoid, RelationGetIndexList(rel)) + { + if (get_index_isclustered(idxoid)) + { + indexOid =3D idxoid; + break; + } + } + + if (!OidIsValid(indexOid)) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("there is no previously clustered index for table \"%s\"", + RelationGetRelationName(rel))); + } + else if (indexname !=3D NULL) + { + /* An index was specified; obtain its OID. */ + indexOid =3D get_relname_relid(indexname, rel->rd_rel->relnamespace); + if (!OidIsValid(indexOid)) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("index \"%s\" for table \"%s\" does not exist", + indexname, RelationGetRelationName(rel))); + } + else + indexOid =3D InvalidOid; + + return indexOid; +} + +static const char * +RepackCommandAsString(RepackCommand cmd) +{ + switch (cmd) + { + case REPACK_COMMAND_REPACK: + return "REPACK"; + case REPACK_COMMAND_VACUUMFULL: + return "VACUUM"; + case REPACK_COMMAND_CLUSTER: + return "CLUSTER"; + } + return "???"; /* keep compiler quiet */ +} diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index 03932f45c8a..aea998260e1 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -351,7 +351,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, boo= l isTopLevel) } } =20 - /* * Sanity check DISABLE_PAGE_SKIPPING option. */ @@ -2289,8 +2288,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParam= s params, if ((params.options & VACOPT_VERBOSE) !=3D 0) cluster_params.options |=3D CLUOPT_VERBOSE; =20 - /* VACUUM FULL is now a variant of CLUSTER; see cluster.c */ - cluster_rel(rel, InvalidOid, &cluster_params); + /* VACUUM FULL is a variant of REPACK; see cluster.c */ + cluster_rel(REPACK_COMMAND_VACUUMFULL, rel, InvalidOid, + &cluster_params); /* cluster_rel closes the relation, but keeps lock */ =20 rel =3D NULL; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 713ee5c10a2..54d37c10447 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -287,7 +287,7 @@ static Node *makeRecursiveViewSelect(char *relname, Lis= t *aliases, Node *query); AlterCompositeTypeStmt AlterUserMappingStmt AlterRoleStmt AlterRoleSetStmt AlterPolicyStmt AlterStatsStmt AlterDefaultPrivilegesStmt DefACLAction - AnalyzeStmt CallStmt ClosePortalStmt ClusterStmt CommentStmt + AnalyzeStmt CallStmt ClosePortalStmt CommentStmt ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt @@ -304,7 +304,7 @@ static Node *makeRecursiveViewSelect(char *relname, Lis= t *aliases, Node *query); GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt Prepar= ableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt - RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRol= eStmt + RemoveFuncStmt RemoveOperStmt RenameStmt RepackStmt ReturnStmt RevokeStm= t RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt SecLabelStmt SelectStmt TransactionStmt TransactionStmtLegacy TruncateSt= mt UnlistenStmt UpdateStmt VacuumStmt @@ -323,7 +323,7 @@ static Node *makeRecursiveViewSelect(char *relname, Lis= t *aliases, Node *query); =20 %type <str> opt_single_name %type <list> opt_qualified_name -%type <boolean> opt_concurrently +%type <boolean> opt_concurrently opt_usingindex %type <dbehavior> opt_drop_behavior %type <list> opt_utility_option_list %type <list> opt_wait_with_clause @@ -773,7 +773,7 @@ static Node *makeRecursiveViewSelect(char *relname, Lis= t *aliases, Node *query); QUOTE QUOTES =20 RANGE READ REAL REASSIGN RECURSIVE REF_P REFERENCES REFERENCING - REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA + REFRESH REINDEX RELATIVE_P RELEASE RENAME REPACK REPEATABLE REPLACE REPLI= CA RESET RESPECT_P RESTART RESTRICT RETURN RETURNING RETURNS REVOKE RIGHT RO= LE ROLLBACK ROLLUP ROUTINE ROUTINES ROW ROWS RULE =20 @@ -1035,7 +1035,6 @@ stmt: | CallStmt | CheckPointStmt | ClosePortalStmt - | ClusterStmt | CommentStmt | ConstraintsSetStmt | CopyStmt @@ -1109,6 +1108,7 @@ stmt: | RemoveFuncStmt | RemoveOperStmt | RenameStmt + | RepackStmt | RevokeStmt | RevokeRoleStmt | RuleStmt @@ -1146,6 +1146,11 @@ opt_concurrently: | /*EMPTY*/ { $$ =3D false; } ; =20 +opt_usingindex: + USING INDEX { $$ =3D true; } + | /* EMPTY */ { $$ =3D false; } + ; + opt_drop_behavior: CASCADE { $$ =3D DROP_CASCADE; } | RESTRICT { $$ =3D DROP_RESTRICT; } @@ -12036,38 +12041,82 @@ CreateConversionStmt: /*************************************************************************= **** * * QUERY: + * REPACK [ (options) ] [ <qualified_name> [ <name_list> ] [ USING INDE= X <index_name> ] ] + * + * obsolete variants: * CLUSTER (options) [ <qualified_name> [ USING <index_name> ] ] * CLUSTER [VERBOSE] [ <qualified_name> [ USING <index_name> ] ] * CLUSTER [VERBOSE] <index_name> ON <qualified_name> (for pre-8.3) * *************************************************************************= ****/ =20 -ClusterStmt: - CLUSTER '(' utility_option_list ')' qualified_name cluster_index_specif= ication +RepackStmt: + REPACK opt_utility_option_list vacuum_relation USING INDEX name { - ClusterStmt *n =3D makeNode(ClusterStmt); + RepackStmt *n =3D makeNode(RepackStmt); =20 - n->relation =3D $5; + n->command =3D REPACK_COMMAND_REPACK; + n->relation =3D (VacuumRelation *) $3; n->indexname =3D $6; + n->usingindex =3D true; + n->params =3D $2; + $$ =3D (Node *) n; + } + | REPACK opt_utility_option_list vacuum_relation opt_usingindex + { + RepackStmt *n =3D makeNode(RepackStmt); + + n->command =3D REPACK_COMMAND_REPACK; + n->relation =3D (VacuumRelation *) $3; + n->indexname =3D NULL; + n->usingindex =3D $4; + n->params =3D $2; + $$ =3D (Node *) n; + } + | REPACK opt_utility_option_list opt_usingindex + { + RepackStmt *n =3D makeNode(RepackStmt); + + n->command =3D REPACK_COMMAND_REPACK; + n->relation =3D NULL; + n->indexname =3D NULL; + n->usingindex =3D $3; + n->params =3D $2; + $$ =3D (Node *) n; + } + | CLUSTER '(' utility_option_list ')' qualified_name cluster_index_spec= ification + { + RepackStmt *n =3D makeNode(RepackStmt); + + n->command =3D REPACK_COMMAND_CLUSTER; + n->relation =3D makeNode(VacuumRelation); + n->relation->relation =3D $5; + n->indexname =3D $6; + n->usingindex =3D true; n->params =3D $3; $$ =3D (Node *) n; } | CLUSTER opt_utility_option_list { - ClusterStmt *n =3D makeNode(ClusterStmt); + RepackStmt *n =3D makeNode(RepackStmt); =20 + n->command =3D REPACK_COMMAND_CLUSTER; n->relation =3D NULL; n->indexname =3D NULL; + n->usingindex =3D true; n->params =3D $2; $$ =3D (Node *) n; } /* unparenthesized VERBOSE kept for pre-14 compatibility */ | CLUSTER opt_verbose qualified_name cluster_index_specification { - ClusterStmt *n =3D makeNode(ClusterStmt); + RepackStmt *n =3D makeNode(RepackStmt); =20 - n->relation =3D $3; + n->command =3D REPACK_COMMAND_CLUSTER; + n->relation =3D makeNode(VacuumRelation); + n->relation->relation =3D $3; n->indexname =3D $4; + n->usingindex =3D true; if ($2) n->params =3D list_make1(makeDefElem("verbose", NULL, @2)); $$ =3D (Node *) n; @@ -12075,20 +12124,25 @@ ClusterStmt: /* unparenthesized VERBOSE kept for pre-17 compatibility */ | CLUSTER VERBOSE { - ClusterStmt *n =3D makeNode(ClusterStmt); + RepackStmt *n =3D makeNode(RepackStmt); =20 + n->command =3D REPACK_COMMAND_CLUSTER; n->relation =3D NULL; n->indexname =3D NULL; + n->usingindex =3D true; n->params =3D list_make1(makeDefElem("verbose", NULL, @2)); $$ =3D (Node *) n; } /* kept for pre-8.3 compatibility */ | CLUSTER opt_verbose name ON qualified_name { - ClusterStmt *n =3D makeNode(ClusterStmt); + RepackStmt *n =3D makeNode(RepackStmt); =20 - n->relation =3D $5; + n->command =3D REPACK_COMMAND_CLUSTER; + n->relation =3D makeNode(VacuumRelation); + n->relation->relation =3D $5; n->indexname =3D $3; + n->usingindex =3D true; if ($2) n->params =3D list_make1(makeDefElem("verbose", NULL, @2)); $$ =3D (Node *) n; @@ -18127,6 +18181,7 @@ unreserved_keyword: | RELATIVE_P | RELEASE | RENAME + | REPACK | REPEATABLE | REPLACE | REPLICA @@ -18764,6 +18819,7 @@ bare_label_keyword: | RELATIVE_P | RELEASE | RENAME + | REPACK | REPEATABLE | REPLACE | REPLICA diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 34dd6e18df5..ca737b05115 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -279,9 +279,9 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) return COMMAND_OK_IN_RECOVERY | COMMAND_OK_IN_READ_ONLY_TXN; } =20 - case T_ClusterStmt: case T_ReindexStmt: case T_VacuumStmt: + case T_RepackStmt: { /* * These commands write WAL, so they're not strictly @@ -856,14 +856,14 @@ standard_ProcessUtility(PlannedStmt *pstmt, ExecuteCallStmt(castNode(CallStmt, parsetree), params, isAtomicContext,= dest); break; =20 - case T_ClusterStmt: - cluster(pstate, (ClusterStmt *) parsetree, isTopLevel); - break; - case T_VacuumStmt: ExecVacuum(pstate, (VacuumStmt *) parsetree, isTopLevel); break; =20 + case T_RepackStmt: + ExecRepack(pstate, (RepackStmt *) parsetree, isTopLevel); + break; + case T_ExplainStmt: ExplainQuery(pstate, (ExplainStmt *) parsetree, params, dest); break; @@ -2865,10 +2865,6 @@ CreateCommandTag(Node *parsetree) tag =3D CMDTAG_CALL; break; =20 - case T_ClusterStmt: - tag =3D CMDTAG_CLUSTER; - break; - case T_VacuumStmt: if (((VacuumStmt *) parsetree)->is_vacuumcmd) tag =3D CMDTAG_VACUUM; @@ -2876,6 +2872,13 @@ CreateCommandTag(Node *parsetree) tag =3D CMDTAG_ANALYZE; break; =20 + case T_RepackStmt: + if (((RepackStmt *) parsetree)->command =3D=3D REPACK_COMMAND_CLUSTER) + tag =3D CMDTAG_CLUSTER; + else + tag =3D CMDTAG_REPACK; + break; + case T_ExplainStmt: tag =3D CMDTAG_EXPLAIN; break; @@ -3517,7 +3520,7 @@ GetCommandLogLevel(Node *parsetree) lev =3D LOGSTMT_ALL; break; =20 - case T_ClusterStmt: + case T_RepackStmt: lev =3D LOGSTMT_DDL; break; =20 diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pg= statfuncs.c index 73ca0bb0b7f..55a69bf681d 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -287,8 +287,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS) cmdtype =3D PROGRESS_COMMAND_VACUUM; else if (pg_strcasecmp(cmd, "ANALYZE") =3D=3D 0) cmdtype =3D PROGRESS_COMMAND_ANALYZE; - else if (pg_strcasecmp(cmd, "CLUSTER") =3D=3D 0) - cmdtype =3D PROGRESS_COMMAND_CLUSTER; + else if (pg_strcasecmp(cmd, "REPACK") =3D=3D 0) + cmdtype =3D PROGRESS_COMMAND_REPACK; else if (pg_strcasecmp(cmd, "CREATE INDEX") =3D=3D 0) cmdtype =3D PROGRESS_COMMAND_CREATE_INDEX; else if (pg_strcasecmp(cmd, "BASEBACKUP") =3D=3D 0) diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 8b91bc00062..2a1bb47ff03 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1267,7 +1267,7 @@ static const char *const sql_commands[] =3D { "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD= ", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", - "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", + "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "REPACK", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", "TABLE", "TRUNCATE", "UNLISTEN", "UPDATE", "VACUUM", "VALUES", @@ -5086,6 +5086,46 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); } =20 +/* REPACK */ + else if (Matches("REPACK")) + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_clusterables, + "(", "USING INDEX"); + else if (Matches("REPACK", "(*)")) + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_clusterables, + "USING INDEX"); + else if (Matches("REPACK", MatchAnyExcept("("))) + COMPLETE_WITH("USING INDEX"); + else if (Matches("REPACK", "(*)", MatchAnyExcept("("))) + COMPLETE_WITH("USING INDEX"); + else if (Matches("REPACK", MatchAny, "USING", "INDEX") || + Matches("REPACK", "(*)", MatchAny, "USING", "INDEX")) + { + set_completion_reference(prev3_wd); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table); + } + /* + * Complete ... [ (*) ] <sth> USING INDEX, with a list of indexes for + * <sth>. + */ + else if (TailMatches(MatchAny, "USING", "INDEX")) + { + set_completion_reference(prev3_wd); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table); + } + else if (HeadMatches("REPACK", "(*") && + !HeadMatches("REPACK", "(*)")) + { + /* + * This fires if we're in an unfinished parenthesized option list. + * get_previous_words treats a completed parenthesized option list as + * one word, so the above test is correct. + */ + if (ends_with(prev_wd, '(') || ends_with(prev_wd, ',')) + COMPLETE_WITH("ANALYZE", "VERBOSE"); + else if (TailMatches("ANALYZE", "VERBOSE")) + COMPLETE_WITH("ON", "OFF"); + } + /* SECURITY LABEL */ else if (Matches("SECURITY")) COMPLETE_WITH("LABEL"); diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile index e6cd9ef4af5..f964a5803ca 100644 --- a/src/bin/scripts/Makefile +++ b/src/bin/scripts/Makefile @@ -23,6 +23,7 @@ PROGRAMS =3D \ dropdb \ dropuser \ pg_isready \ + pg_repackdb \ reindexdb \ vacuumdb =20 @@ -39,6 +40,7 @@ clusterdb: clusterdb.o common.o $(WIN32RES) | submake-lib= pq submake-libpgport su vacuumdb: vacuumdb.o vacuuming.o common.o $(WIN32RES) | submake-libpq subm= ake-libpgport submake-libpgfeutils reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgp= ort submake-libpgfeutils pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libp= gport submake-libpgfeutils +pg_repackdb: pg_repackdb.o vacuuming.o common.o $(WIN32RES) | submake-libp= q submake-libpgport submake-libpgfeutils =20 install: all installdirs $(INSTALL_PROGRAM) createdb$(X) '$(DESTDIR)$(bindir)'/createdb$(X) @@ -49,6 +51,7 @@ install: all installdirs $(INSTALL_PROGRAM) vacuumdb$(X) '$(DESTDIR)$(bindir)'/vacuumdb$(X) $(INSTALL_PROGRAM) reindexdb$(X) '$(DESTDIR)$(bindir)'/reindexdb$(X) $(INSTALL_PROGRAM) pg_isready$(X) '$(DESTDIR)$(bindir)'/pg_isready$(X) + $(INSTALL_PROGRAM) pg_repackdb$(X) '$(DESTDIR)$(bindir)'/pg_repackdb$(X) =20 installdirs: $(MKDIR_P) '$(DESTDIR)$(bindir)' diff --git a/src/bin/scripts/meson.build b/src/bin/scripts/meson.build index c083ec38099..1e88bacebba 100644 --- a/src/bin/scripts/meson.build +++ b/src/bin/scripts/meson.build @@ -42,6 +42,7 @@ vacuuming_common =3D static_library('libvacuuming_common', =20 binaries =3D [ 'vacuumdb', + 'pg_repackdb', ] foreach binary : binaries binary_sources =3D files('@[email protected]'.format(binary)) @@ -80,6 +81,7 @@ tests +=3D { 't/100_vacuumdb.pl', 't/101_vacuumdb_all.pl', 't/102_vacuumdb_stages.pl', + 't/103_repackdb.pl', 't/200_connstr.pl', ], }, diff --git a/src/bin/scripts/pg_repackdb.c b/src/bin/scripts/pg_repackdb.c new file mode 100644 index 00000000000..2765d1e97b8 --- /dev/null +++ b/src/bin/scripts/pg_repackdb.c @@ -0,0 +1,240 @@ +/*------------------------------------------------------------------------- + * + * pg_repackdb + * An utility to run REPACK + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * FIXME: this is missing a way to specify the index to use to repack one + * table, or whether to pass a WITH INDEX clause when multiple tables are + * used. Something like --index[=3Dindexname]. Adding that bleeds into + * vacuuming.c as well. + * + * src/bin/scripts/pg_repackdb.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include <limits.h> + +#include "common.h" +#include "common/logging.h" +#include "fe_utils/option_utils.h" +#include "vacuuming.h" + +static void help(const char *progname); +static void check_objfilter(bits32 objfilter); + +int +main(int argc, char *argv[]) +{ + static struct option long_options[] =3D { + {"host", required_argument, NULL, 'h'}, + {"port", required_argument, NULL, 'p'}, + {"username", required_argument, NULL, 'U'}, + {"no-password", no_argument, NULL, 'w'}, + {"password", no_argument, NULL, 'W'}, + {"echo", no_argument, NULL, 'e'}, + {"quiet", no_argument, NULL, 'q'}, + {"dbname", required_argument, NULL, 'd'}, + {"analyze", no_argument, NULL, 'z'}, + {"all", no_argument, NULL, 'a'}, + /* XXX this could be 'i', but optional_arg is messy */ + {"index", optional_argument, NULL, 1}, + {"table", required_argument, NULL, 't'}, + {"verbose", no_argument, NULL, 'v'}, + {"jobs", required_argument, NULL, 'j'}, + {"schema", required_argument, NULL, 'n'}, + {"exclude-schema", required_argument, NULL, 'N'}, + {"maintenance-db", required_argument, NULL, 2}, + {NULL, 0, NULL, 0} + }; + + const char *progname; + int optindex; + int c; + const char *dbname =3D NULL; + const char *maintenance_db =3D NULL; + ConnParams cparams; + vacuumingOptions vacopts; + SimpleStringList objects =3D {NULL, NULL}; + int concurrentCons =3D 1; + int tbl_count =3D 0; + int ret; + + /* initialize options */ + memset(&vacopts, 0, sizeof(vacopts)); + vacopts.mode =3D MODE_REPACK; + + /* the same for connection parameters */ + memset(&cparams, 0, sizeof(cparams)); + cparams.prompt_password =3D TRI_DEFAULT; + + pg_logging_init(argv[0]); + progname =3D get_progname(argv[0]); + set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pgscripts")); + + handle_help_version_opts(argc, argv, progname, help); + + while ((c =3D getopt_long(argc, argv, "ad:eh:j:n:N:p:qt:U:vwWz", + long_options, &optindex)) !=3D -1) + { + switch (c) + { + case 'a': + vacopts.objfilter |=3D OBJFILTER_ALL_DBS; + break; + case 'd': + vacopts.objfilter |=3D OBJFILTER_DATABASE; + dbname =3D pg_strdup(optarg); + break; + case 'e': + vacopts.echo =3D true; + break; + case 'h': + cparams.pghost =3D pg_strdup(optarg); + break; + case 'j': + if (!option_parse_int(optarg, "-j/--jobs", 1, INT_MAX, + &concurrentCons)) + exit(1); + break; + case 'n': + vacopts.objfilter |=3D OBJFILTER_SCHEMA; + simple_string_list_append(&objects, optarg); + break; + case 'N': + vacopts.objfilter |=3D OBJFILTER_SCHEMA_EXCLUDE; + simple_string_list_append(&objects, optarg); + break; + case 'p': + cparams.pgport =3D pg_strdup(optarg); + break; + case 'q': + vacopts.quiet =3D true; + break; + case 't': + vacopts.objfilter |=3D OBJFILTER_TABLE; + simple_string_list_append(&objects, optarg); + tbl_count++; + break; + case 'U': + cparams.pguser =3D pg_strdup(optarg); + break; + case 'v': + vacopts.verbose =3D true; + break; + case 'w': + cparams.prompt_password =3D TRI_NO; + break; + case 'W': + cparams.prompt_password =3D TRI_YES; + break; + case 'z': + vacopts.and_analyze =3D true; + break; + case 1: + vacopts.using_index =3D true; + if (optarg) + vacopts.indexname =3D pg_strdup(optarg); + else + vacopts.indexname =3D NULL; + break; + case 2: + maintenance_db =3D pg_strdup(optarg); + break; + default: + /* getopt_long already emitted a complaint */ + pg_log_error_hint("Try \"%s --help\" for more information.", progname); + exit(1); + } + } + + /* + * Non-option argument specifies database name as long as it wasn't + * already specified with -d / --dbname + */ + if (optind < argc && dbname =3D=3D NULL) + { + vacopts.objfilter |=3D OBJFILTER_DATABASE; + dbname =3D argv[optind]; + optind++; + } + + if (optind < argc) + { + pg_log_error("too many command-line arguments (first is \"%s\")", + argv[optind]); + pg_log_error_hint("Try \"%s --help\" for more information.", progname); + exit(1); + } + + /* + * Validate the combination of filters specified in the command-line + * options. + */ + check_objfilter(vacopts.objfilter); + + ret =3D vacuuming_main(&cparams, dbname, maintenance_db, &vacopts, + &objects, tbl_count, concurrentCons, + progname); + exit(ret); +} + +/* + * Verify that the filters used at command line are compatible. + */ +void +check_objfilter(bits32 objfilter) +{ + if ((objfilter & OBJFILTER_ALL_DBS) && + (objfilter & OBJFILTER_DATABASE)) + pg_fatal("cannot repack all databases and a specific one at the same tim= e"); + + if ((objfilter & OBJFILTER_TABLE) && + (objfilter & OBJFILTER_SCHEMA)) + pg_fatal("cannot repack all tables in schema(s) and specific table(s) at= the same time"); + + if ((objfilter & OBJFILTER_TABLE) && + (objfilter & OBJFILTER_SCHEMA_EXCLUDE)) + pg_fatal("cannot repack specific table(s) and exclude schema(s) at the s= ame time"); + + if ((objfilter & OBJFILTER_SCHEMA) && + (objfilter & OBJFILTER_SCHEMA_EXCLUDE)) + pg_fatal("cannot repack all tables in schema(s) and exclude schema(s) at= the same time"); +} + +static void +help(const char *progname) +{ + printf(_("%s repacks a PostgreSQL database.\n\n"), progname); + printf(_("Usage:\n")); + printf(_(" %s [OPTION]... [DBNAME]\n"), progname); + printf(_("\nOptions:\n")); + printf(_(" -a, --all repack all databases\n")); + printf(_(" -d, --dbname=3DDBNAME database to repack\n")); + printf(_(" -e, --echo show the commands being sent = to the server\n")); + printf(_(" --index[=3DINDEX] repack following an index\n= ")); + printf(_(" -j, --jobs=3DNUM use this many concurrent co= nnections to repack\n")); + printf(_(" -n, --schema=3DSCHEMA repack tables in the specif= ied schema(s) only\n")); + printf(_(" -N, --exclude-schema=3DSCHEMA do not repack tables in the= specified schema(s)\n")); + printf(_(" -q, --quiet don't write any messages\n")); + printf(_(" -t, --table=3D'TABLE[(COLUMNS)]' repack specific table(s) on= ly\n")); + printf(_(" -v, --verbose write a lot of output\n")); + printf(_(" -V, --version output version information, t= hen exit\n")); + printf(_(" -z, --analyze update optimizer statistics\n= ")); + printf(_(" -?, --help show this help, then exit\n")= ); + printf(_("\nConnection options:\n")); + printf(_(" -h, --host=3DHOSTNAME database server host or socket di= rectory\n")); + printf(_(" -p, --port=3DPORT database server port\n")); + printf(_(" -U, --username=3DUSERNAME user name to connect as\n")); + printf(_(" -w, --no-password never prompt for password\n")); + printf(_(" -W, --password force password prompt\n")); + printf(_(" --maintenance-db=3DDBNAME alternate maintenance database\n"= )); + printf(_("\nRead the description of the SQL command REPACK for details.\n= ")); + printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT); + printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL); +} diff --git a/src/bin/scripts/t/103_repackdb.pl b/src/bin/scripts/t/103_repa= ckdb.pl new file mode 100644 index 00000000000..44113367a61 --- /dev/null +++ b/src/bin/scripts/t/103_repackdb.pl @@ -0,0 +1,47 @@ +# Copyright (c) 2021-2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL =3D> 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +program_help_ok('pg_repackdb'); +program_version_ok('pg_repackdb'); +program_options_handling_ok('pg_repackdb'); + +my $node =3D PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; + +$node->issues_sql_like( + [ 'pg_repackdb', 'postgres', '-t', 'pg_class'], + qr/statement: REPACK.*pg_class;/, + 'pg_repackdb processes a single table'); + +$node->safe_psql('postgres', 'CREATE USER testusr; + GRANT CREATE ON SCHEMA public TO testusr'); +$node->safe_psql('postgres', + 'CREATE TABLE cluster_1 (a int primary key); + ALTER TABLE cluster_1 CLUSTER ON cluster_1_pkey; + CREATE TABLE cluster_2 (a int unique); + ALTER TABLE cluster_2 CLUSTER ON cluster_2_a_key;', + extra_params =3D> ['-U' =3D> 'testusr']); + +$node->issues_sql_like( + [ 'pg_repackdb', 'postgres', '-U', 'testusr' ], + qr/statement: REPACK.*;/, + 'SQL REPACK run'); + +$node->issues_sql_like( + [ 'pg_repackdb', 'postgres', '--index'], + qr/statement: REPACK.*cluster_1 USING INDEX.*statement: REPACK.*cluster_2= USING INDEX/ms, + 'pg_repackdb --index chooses multiple tables'); + +$node->issues_sql_like( + [ 'pg_repackdb', 'postgres', '--analyze', '-t', 'cluster_1'], + qr/statement: REPACK \(ANALYZE\) public.cluster_1/, + 'pg_repackdb --analyze works'); + +done_testing(); diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c index faac9089a01..0fa7be3c0c3 100644 --- a/src/bin/scripts/vacuuming.c +++ b/src/bin/scripts/vacuuming.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * vacuuming.c - * Helper routines for vacuumdb + * Helper routines for vacuumdb and pg_repackdb * * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California @@ -194,6 +194,14 @@ vacuum_one_database(ConnParams *cparams, =20 conn =3D connectDatabase(cparams, progname, vacopts->echo, false, true); =20 + if (vacopts->mode =3D=3D MODE_REPACK && PQserverVersion(conn) < 190000) + { + /* XXX arguably, here we should use VACUUM FULL instead of failing */ + PQfinish(conn); + pg_fatal("cannot use the \"%s\" command on server versions older than Po= stgreSQL %s", + "REPACK", "19"); + } + if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600) { PQfinish(conn); @@ -286,9 +294,18 @@ vacuum_one_database(ConnParams *cparams, if (vacopts->mode =3D=3D MODE_ANALYZE_IN_STAGES) printf(_("%s: processing database \"%s\": %s\n"), progname, PQdb(conn), _(stage_messages[stage])); - else + else if (vacopts->mode =3D=3D MODE_ANALYZE) + printf(_("%s: analyzing database \"%s\"\n"), + progname, PQdb(conn)); + else if (vacopts->mode =3D=3D MODE_VACUUM) printf(_("%s: vacuuming database \"%s\"\n"), progname, PQdb(conn)); + else + { + Assert(vacopts->mode =3D=3D MODE_REPACK); + printf(_("%s: repacking database \"%s\"\n"), + progname, PQdb(conn)); + } fflush(stdout); } =20 @@ -640,6 +657,35 @@ retrieve_objects(PGconn *conn, vacuumingOptions *vacop= ts, " AND listed_objects.object_oid IS NOT NULL\n"); } =20 + /* + * In REPACK mode, if the 'using_index' option was given but no index + * name, filter only tables that have an index with indisclustered set. + * (If an index name is given, we trust the user to pass a reasonable list + * of tables.) + * + * XXX it may be worth printing an error if an index name is given with no + * list of tables. + */ + if (vacopts->mode =3D=3D MODE_REPACK && + vacopts->using_index && !vacopts->indexname) + { + appendPQExpBufferStr(&catalog_query, + " AND EXISTS (SELECT 1 FROM pg_catalog.pg_index\n" + " WHERE indrelid =3D c.oid AND indisclustered)\n"); + } + + /* + * In REPACK mode, only consider the tables that the current user has + * MAINTAIN privileges on. XXX maybe we should do this in all cases, not + * just REPACK. The vacuumdb output is too noisy for no reason. + */ + if (vacopts->mode =3D=3D MODE_REPACK) + { + appendPQExpBufferStr(&catalog_query, + " AND pg_catalog.has_table_privilege(current_user, " + "c.oid, 'MAINTAIN')\n"); + } + /* * If no tables were listed, filter for the relevant relation types. If * tables were given via --table, don't bother filtering by relation type. @@ -878,8 +924,10 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql, if (vacopts->verbose) appendPQExpBufferStr(sql, " VERBOSE"); } + + appendPQExpBuffer(sql, " %s", table); } - else + else if (vacopts->mode =3D=3D MODE_VACUUM) { appendPQExpBufferStr(sql, "VACUUM"); =20 @@ -993,9 +1041,39 @@ prepare_vacuum_command(PGconn *conn, PQExpBuffer sql, if (vacopts->and_analyze) appendPQExpBufferStr(sql, " ANALYZE"); } + + appendPQExpBuffer(sql, " %s", table); } + else if (vacopts->mode =3D=3D MODE_REPACK) + { + appendPQExpBufferStr(sql, "REPACK"); =20 - appendPQExpBuffer(sql, " %s;", table); + if (vacopts->verbose) + { + appendPQExpBuffer(sql, "%sVERBOSE", sep); + sep =3D comma; + } + if (vacopts->and_analyze) + { + appendPQExpBuffer(sql, "%sANALYZE", sep); + sep =3D comma; + } + + if (sep !=3D paren) + appendPQExpBufferChar(sql, ')'); + + appendPQExpBuffer(sql, " %s", table); + + if (vacopts->using_index) + { + appendPQExpBuffer(sql, " USING INDEX"); + if (vacopts->indexname) + appendPQExpBuffer(sql, " %s", fmtIdEnc(vacopts->indexname, + PQclientEncoding(conn))); + } + } + + appendPQExpBufferChar(sql, ';'); } =20 /* @@ -1024,13 +1102,21 @@ run_vacuum_command(ParallelSlot *free_slot, vacuumi= ngOptions *vacopts, { if (table) { - pg_log_error("vacuuming of table \"%s\" in database \"%s\" failed: %s", - table, PQdb(conn), PQerrorMessage(conn)); + if (vacopts->mode =3D=3D MODE_VACUUM) + pg_log_error("vacuuming of table \"%s\" in database \"%s\" failed: %s", + table, PQdb(conn), PQerrorMessage(conn)); + else + pg_log_error("repacking of table \"%s\" in database \"%s\" failed: %s", + table, PQdb(conn), PQerrorMessage(conn)); } else { - pg_log_error("vacuuming of database \"%s\" failed: %s", - PQdb(conn), PQerrorMessage(conn)); + if (vacopts->mode =3D=3D MODE_VACUUM) + pg_log_error("vacuuming of database \"%s\" failed: %s", + PQdb(conn), PQerrorMessage(conn)); + else + pg_log_error("repacking of database \"%s\" failed: %s", + PQdb(conn), PQerrorMessage(conn)); } } } diff --git a/src/bin/scripts/vacuuming.h b/src/bin/scripts/vacuuming.h index be4a75ef2f4..6e8abe00a4f 100644 --- a/src/bin/scripts/vacuuming.h +++ b/src/bin/scripts/vacuuming.h @@ -20,6 +20,7 @@ typedef enum { MODE_VACUUM, + MODE_REPACK, MODE_ANALYZE, MODE_ANALYZE_IN_STAGES } RunMode; @@ -37,6 +38,8 @@ typedef struct vacuumingOptions bool and_analyze; bool full; bool freeze; + bool using_index; + char *indexname; bool disable_page_skipping; bool skip_locked; int min_xid_age; diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h index 8ea81622f9d..28741988478 100644 --- a/src/include/commands/cluster.h +++ b/src/include/commands/cluster.h @@ -24,6 +24,7 @@ #define CLUOPT_RECHECK 0x02 /* recheck relation state */ #define CLUOPT_RECHECK_ISCLUSTERED 0x04 /* recheck relation state for * indisclustered */ +#define CLUOPT_ANALYZE 0x08 /* do an ANALYZE */ =20 /* options for CLUSTER */ typedef struct ClusterParams @@ -31,8 +32,11 @@ typedef struct ClusterParams bits32 options; /* bitmask of CLUOPT_* */ } ClusterParams; =20 -extern void cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel= ); -extern void cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *par= ams); + +extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLev= el); + +extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid index= Oid, + ClusterParams *params); extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode); extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_inter= nal); diff --git a/src/include/commands/progress.h b/src/include/commands/progres= s.h index 359221dc296..f00e39b937d 100644 --- a/src/include/commands/progress.h +++ b/src/include/commands/progress.h @@ -73,28 +73,34 @@ #define PROGRESS_ANALYZE_STARTED_BY_MANUAL 1 #define PROGRESS_ANALYZE_STARTED_BY_AUTOVACUUM 2 =20 -/* Progress parameters for cluster */ -#define PROGRESS_CLUSTER_COMMAND 0 -#define PROGRESS_CLUSTER_PHASE 1 -#define PROGRESS_CLUSTER_INDEX_RELID 2 -#define PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED 3 -#define PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN 4 -#define PROGRESS_CLUSTER_TOTAL_HEAP_BLKS 5 -#define PROGRESS_CLUSTER_HEAP_BLKS_SCANNED 6 -#define PROGRESS_CLUSTER_INDEX_REBUILD_COUNT 7 - -/* Phases of cluster (as advertised via PROGRESS_CLUSTER_PHASE) */ -#define PROGRESS_CLUSTER_PHASE_SEQ_SCAN_HEAP 1 -#define PROGRESS_CLUSTER_PHASE_INDEX_SCAN_HEAP 2 -#define PROGRESS_CLUSTER_PHASE_SORT_TUPLES 3 -#define PROGRESS_CLUSTER_PHASE_WRITE_NEW_HEAP 4 -#define PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES 5 -#define PROGRESS_CLUSTER_PHASE_REBUILD_INDEX 6 -#define PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP 7 - -/* Commands of PROGRESS_CLUSTER */ -#define PROGRESS_CLUSTER_COMMAND_CLUSTER 1 -#define PROGRESS_CLUSTER_COMMAND_VACUUM_FULL 2 +/* + * Progress parameters for REPACK. + * + * Values for PROGRESS_REPACK_COMMAND are defined as in RepackCommand. + * + * Note: Since REPACK shares code with CLUSTER, these values are also + * used by CLUSTER. (CLUSTER is now deprecated, so it makes little sense to + * introduce a separate set of constants.) + */ +#define PROGRESS_REPACK_COMMAND 0 +#define PROGRESS_REPACK_PHASE 1 +#define PROGRESS_REPACK_INDEX_RELID 2 +#define PROGRESS_REPACK_HEAP_TUPLES_SCANNED 3 +#define PROGRESS_REPACK_HEAP_TUPLES_WRITTEN 4 +#define PROGRESS_REPACK_TOTAL_HEAP_BLKS 5 +#define PROGRESS_REPACK_HEAP_BLKS_SCANNED 6 +#define PROGRESS_REPACK_INDEX_REBUILD_COUNT 7 + +/* + * Phases of repack (as advertised via PROGRESS_REPACK_PHASE). + */ +#define PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP 1 +#define PROGRESS_REPACK_PHASE_INDEX_SCAN_HEAP 2 +#define PROGRESS_REPACK_PHASE_SORT_TUPLES 3 +#define PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP 4 +#define PROGRESS_REPACK_PHASE_SWAP_REL_FILES 5 +#define PROGRESS_REPACK_PHASE_REBUILD_INDEX 6 +#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP 7 =20 /* Progress parameters for CREATE INDEX */ /* 3, 4 and 5 reserved for "waitfor" metrics */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 646d6ced763..c15ba5e6f29 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3980,18 +3980,6 @@ typedef struct AlterSystemStmt VariableSetStmt *setstmt; /* SET subcommand */ } AlterSystemStmt; =20 -/* ---------------------- - * Cluster Statement (support pbrown's cluster index implementation) - * ---------------------- - */ -typedef struct ClusterStmt -{ - NodeTag type; - RangeVar *relation; /* relation being indexed, or NULL if all */ - char *indexname; /* original index defined */ - List *params; /* list of DefElem nodes */ -} ClusterStmt; - /* ---------------------- * Vacuum and Analyze Statements * @@ -4004,7 +3992,7 @@ typedef struct VacuumStmt NodeTag type; List *options; /* list of DefElem nodes */ List *rels; /* list of VacuumRelation, or NIL for all */ - bool is_vacuumcmd; /* true for VACUUM, false for ANALYZE */ + bool is_vacuumcmd; /* true for VACUUM, false otherwise */ } VacuumStmt; =20 /* @@ -4022,6 +4010,27 @@ typedef struct VacuumRelation List *va_cols; /* list of column names, or NIL for all */ } VacuumRelation; =20 +/* ---------------------- + * Repack Statement + * ---------------------- + */ +typedef enum RepackCommand +{ + REPACK_COMMAND_CLUSTER =3D 1, + REPACK_COMMAND_REPACK, + REPACK_COMMAND_VACUUMFULL, +} RepackCommand; + +typedef struct RepackStmt +{ + NodeTag type; + RepackCommand command; /* type of command being run */ + VacuumRelation *relation; /* relation being repacked */ + char *indexname; /* order tuples by this index */ + bool usingindex; /* whether USING INDEX is specified */ + List *params; /* list of DefElem nodes */ +} RepackStmt; + /* ---------------------- * Explain Statement * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f7753c5c8a8..6f74a8c05c7 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -377,6 +377,7 @@ PG_KEYWORD("reindex", REINDEX, UNRESERVED_KEYWORD, BARE= _LABEL) PG_KEYWORD("relative", RELATIVE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("release", RELEASE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("rename", RENAME, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("repack", REPACK, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("repeatable", REPEATABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("replace", REPLACE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("replica", REPLICA, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 1290c9bab68..652dc61b834 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -196,6 +196,7 @@ PG_CMDTAG(CMDTAG_REASSIGN_OWNED, "REASSIGN OWNED", fals= e, false, false) PG_CMDTAG(CMDTAG_REFRESH_MATERIALIZED_VIEW, "REFRESH MATERIALIZED VIEW", t= rue, false, false) PG_CMDTAG(CMDTAG_REINDEX, "REINDEX", true, false, false) PG_CMDTAG(CMDTAG_RELEASE, "RELEASE", false, false, false) +PG_CMDTAG(CMDTAG_REPACK, "REPACK", false, false, false) PG_CMDTAG(CMDTAG_RESET, "RESET", false, false, false) PG_CMDTAG(CMDTAG_REVOKE, "REVOKE", true, false, false) PG_CMDTAG(CMDTAG_REVOKE_ROLE, "REVOKE ROLE", false, false, false) diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backe= nd_progress.h index 19f63b41431..6300dbd15d5 100644 --- a/src/include/utils/backend_progress.h +++ b/src/include/utils/backend_progress.h @@ -24,10 +24,10 @@ typedef enum ProgressCommandType PROGRESS_COMMAND_INVALID, PROGRESS_COMMAND_VACUUM, PROGRESS_COMMAND_ANALYZE, - PROGRESS_COMMAND_CLUSTER, PROGRESS_COMMAND_CREATE_INDEX, PROGRESS_COMMAND_BASEBACKUP, PROGRESS_COMMAND_COPY, + PROGRESS_COMMAND_REPACK, } ProgressCommandType; =20 #define PGSTAT_NUM_PROGRESS_PARAM 20 diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expec= ted/cluster.out index 4d40a6809ab..277854418fa 100644 --- a/src/test/regress/expected/cluster.out +++ b/src/test/regress/expected/cluster.out @@ -495,6 +495,43 @@ ALTER TABLE clstrpart SET WITHOUT CLUSTER; ERROR: cannot mark index clustered in partitioned table ALTER TABLE clstrpart CLUSTER ON clstrpart_idx; ERROR: cannot mark index clustered in partitioned table +-- Check that REPACK sets new relfilenodes: it should process exactly the = same +-- tables as CLUSTER did. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, = relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class= c ON c.oid=3Dtree.relid ; +REPACK clstrpart USING INDEX clstrpart_idx; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, = relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class= c ON c.oid=3Dtree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode =3D new.relfilenod= e FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname)= ORDER BY relname COLLATE "C"; + relname | level | relkind | ?column?=20 +-------------+-------+---------+---------- + clstrpart | 0 | p | t + clstrpart1 | 1 | p | t + clstrpart11 | 2 | r | f + clstrpart12 | 2 | p | t + clstrpart2 | 1 | r | f + clstrpart3 | 1 | p | t + clstrpart33 | 2 | r | f +(7 rows) + +-- And finally the same for REPACK w/o index. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, = relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class= c ON c.oid=3Dtree.relid ; +REPACK clstrpart; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, = relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class= c ON c.oid=3Dtree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode =3D new.relfilenod= e FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname)= ORDER BY relname COLLATE "C"; + relname | level | relkind | ?column?=20 +-------------+-------+---------+---------- + clstrpart | 0 | p | t + clstrpart1 | 1 | p | t + clstrpart11 | 2 | r | f + clstrpart12 | 2 | p | t + clstrpart2 | 1 | r | f + clstrpart3 | 1 | p | t + clstrpart33 | 2 | r | f +(7 rows) + DROP TABLE clstrpart; -- Ownership of partitions is checked CREATE TABLE ptnowner(i int unique) PARTITION BY LIST (i); @@ -513,7 +550,7 @@ CREATE TEMP TABLE ptnowner_oldnodes AS JOIN pg_class AS c ON c.oid=3Dtree.relid; SET SESSION AUTHORIZATION regress_ptnowner; CLUSTER ptnowner USING ptnowner_i_idx; -WARNING: permission denied to cluster "ptnowner2", skipping it +WARNING: permission denied to execute CLUSTER on "ptnowner2", skipping it RESET SESSION AUTHORIZATION; SELECT a.relname, a.relfilenode=3Db.relfilenode FROM pg_class a JOIN ptnowner_oldnodes b USING (oid) ORDER BY a.relname COLLATE "C"; @@ -665,6 +702,101 @@ SELECT * FROM clstr_expression WHERE -a =3D -3 ORDER = BY -a, b; (4 rows) =20 COMMIT; +---------------------------------------------------------------------- +-- +-- REPACK +-- +---------------------------------------------------------------------- +-- REPACK handles individual tables identically to CLUSTER, but it's worth +-- checking if it handles table hierarchies identically as well. +REPACK clstr_tst USING INDEX clstr_tst_c; +-- Verify that inheritance link still works +INSERT INTO clstr_tst_inh VALUES (0, 100, 'in child table 2'); +SELECT a,b,c,substring(d for 30), length(d) from clstr_tst; + a | b | c | substring | length=20 +----+-----+------------------+--------------------------------+-------- + 10 | 14 | catorce | |=20=20=20= =20=20=20=20 + 18 | 5 | cinco | |=20=20=20= =20=20=20=20 + 9 | 4 | cuatro | |=20=20=20= =20=20=20=20 + 26 | 19 | diecinueve | |=20=20=20= =20=20=20=20 + 12 | 18 | dieciocho | |=20=20=20= =20=20=20=20 + 30 | 16 | dieciseis | |=20=20=20= =20=20=20=20 + 24 | 17 | diecisiete | |=20=20=20= =20=20=20=20 + 2 | 10 | diez | |=20=20=20= =20=20=20=20 + 23 | 12 | doce | |=20=20=20= =20=20=20=20 + 11 | 2 | dos | |=20=20=20= =20=20=20=20 + 25 | 9 | nueve | |=20=20=20= =20=20=20=20 + 31 | 8 | ocho | |=20=20=20= =20=20=20=20 + 1 | 11 | once | |=20=20=20= =20=20=20=20 + 28 | 15 | quince | |=20=20=20= =20=20=20=20 + 32 | 6 | seis | xyzzyxyzzyxyzzyxyzzyxyzzyxyzzy | 500000 + 29 | 7 | siete | |=20=20=20= =20=20=20=20 + 15 | 13 | trece | |=20=20=20= =20=20=20=20 + 22 | 30 | treinta | |=20=20=20= =20=20=20=20 + 17 | 32 | treinta y dos | |=20=20=20= =20=20=20=20 + 3 | 31 | treinta y uno | |=20=20=20= =20=20=20=20 + 5 | 3 | tres | |=20=20=20= =20=20=20=20 + 20 | 1 | uno | |=20=20=20= =20=20=20=20 + 6 | 20 | veinte | |=20=20=20= =20=20=20=20 + 14 | 25 | veinticinco | |=20=20=20= =20=20=20=20 + 21 | 24 | veinticuatro | |=20=20=20= =20=20=20=20 + 4 | 22 | veintidos | |=20=20=20= =20=20=20=20 + 19 | 29 | veintinueve | |=20=20=20= =20=20=20=20 + 16 | 28 | veintiocho | |=20=20=20= =20=20=20=20 + 27 | 26 | veintiseis | |=20=20=20= =20=20=20=20 + 13 | 27 | veintisiete | |=20=20=20= =20=20=20=20 + 7 | 23 | veintitres | |=20=20=20= =20=20=20=20 + 8 | 21 | veintiuno | |=20=20=20= =20=20=20=20 + 0 | 100 | in child table | |=20=20=20= =20=20=20=20 + 0 | 100 | in child table 2 | |=20=20=20= =20=20=20=20 +(34 rows) + +-- Verify that foreign key link still works +INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail'); +ERROR: insert or update on table "clstr_tst" violates foreign key constra= int "clstr_tst_con" +DETAIL: Key (b)=3D(1111) is not present in table "clstr_tst_s". +SELECT conname FROM pg_constraint WHERE conrelid =3D 'clstr_tst'::regclass +ORDER BY 1; + conname=20=20=20=20=20=20=20=20 +---------------------- + clstr_tst_a_not_null + clstr_tst_con + clstr_tst_pkey +(3 rows) + +-- Verify partial analyze works +REPACK (ANALYZE) clstr_tst (a); +REPACK (ANALYZE) clstr_tst; +REPACK (VERBOSE) clstr_tst (a); +ERROR: ANALYZE option must be specified when a column list is provided +-- REPACK w/o argument performs no ordering, so we can only check which ta= bles +-- have the relfilenode changed. +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_old AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); +SET SESSION AUTHORIZATION regress_clstr_user; +SET client_min_messages =3D ERROR; -- order of "skipping" warnings may va= ry +REPACK; +RESET client_min_messages; +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_new AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); +-- Do the actual comparison. Unlike CLUSTER, clstr_3 should have been +-- processed because there is nothing like clustering index here. +SELECT o.relname FROM relnodes_old o +JOIN relnodes_new n ON o.relname =3D n.relname +WHERE o.relfilenode <> n.relfilenode +ORDER BY o.relname; + relname=20 +--------- + clstr_1 + clstr_3 +(2 rows) + -- clean up DROP TABLE clustertest; DROP TABLE clstr_1; diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expecte= d/rules.out index f4ee2bd7459..48461550636 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -2002,34 +2002,23 @@ pg_stat_progress_basebackup| SELECT pid, ELSE NULL::text END AS backup_type FROM pg_stat_get_progress_info('BASEBACKUP'::text) s(pid, datid, relid,= param1, param2, param3, param4, param5, param6, param7, param8, param9, pa= ram10, param11, param12, param13, param14, param15, param16, param17, param= 18, param19, param20); -pg_stat_progress_cluster| SELECT s.pid, - s.datid, - d.datname, - s.relid, - CASE s.param1 - WHEN 1 THEN 'CLUSTER'::text - WHEN 2 THEN 'VACUUM FULL'::text - ELSE NULL::text +pg_stat_progress_cluster| SELECT pid, + datid, + datname, + relid, + CASE + WHEN (command =3D ANY (ARRAY['CLUSTER'::text, 'VACUUM FULL'::t= ext])) THEN command + WHEN (repack_index_relid =3D (0)::oid) THEN 'VACUUM FULL'::text + ELSE 'CLUSTER'::text END AS command, - CASE s.param2 - WHEN 0 THEN 'initializing'::text - WHEN 1 THEN 'seq scanning heap'::text - WHEN 2 THEN 'index scanning heap'::text - WHEN 3 THEN 'sorting tuples'::text - WHEN 4 THEN 'writing new heap'::text - WHEN 5 THEN 'swapping relation files'::text - WHEN 6 THEN 'rebuilding index'::text - WHEN 7 THEN 'performing final cleanup'::text - ELSE NULL::text - END AS phase, - (s.param3)::oid AS cluster_index_relid, - s.param4 AS heap_tuples_scanned, - s.param5 AS heap_tuples_written, - s.param6 AS heap_blks_total, - s.param7 AS heap_blks_scanned, - s.param8 AS index_rebuild_count - FROM (pg_stat_get_progress_info('CLUSTER'::text) s(pid, datid, relid, p= aram1, param2, param3, param4, param5, param6, param7, param8, param9, para= m10, param11, param12, param13, param14, param15, param16, param17, param18= , param19, param20) - LEFT JOIN pg_database d ON ((s.datid =3D d.oid))); + phase, + repack_index_relid AS cluster_index_relid, + heap_tuples_scanned, + heap_tuples_written, + heap_blks_total, + heap_blks_scanned, + index_rebuild_count + FROM pg_stat_progress_repack; pg_stat_progress_copy| SELECT s.pid, s.datid, d.datname, @@ -2089,6 +2078,35 @@ pg_stat_progress_create_index| SELECT s.pid, s.param15 AS partitions_done FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, rel= id, param1, param2, param3, param4, param5, param6, param7, param8, param9,= param10, param11, param12, param13, param14, param15, param16, param17, pa= ram18, param19, param20) LEFT JOIN pg_database d ON ((s.datid =3D d.oid))); +pg_stat_progress_repack| SELECT s.pid, + s.datid, + d.datname, + s.relid, + CASE s.param1 + WHEN 1 THEN 'CLUSTER'::text + WHEN 2 THEN 'REPACK'::text + WHEN 3 THEN 'VACUUM FULL'::text + ELSE NULL::text + END AS command, + CASE s.param2 + WHEN 0 THEN 'initializing'::text + WHEN 1 THEN 'seq scanning heap'::text + WHEN 2 THEN 'index scanning heap'::text + WHEN 3 THEN 'sorting tuples'::text + WHEN 4 THEN 'writing new heap'::text + WHEN 5 THEN 'swapping relation files'::text + WHEN 6 THEN 'rebuilding index'::text + WHEN 7 THEN 'performing final cleanup'::text + ELSE NULL::text + END AS phase, + (s.param3)::oid AS repack_index_relid, + s.param4 AS heap_tuples_scanned, + s.param5 AS heap_tuples_written, + s.param6 AS heap_blks_total, + s.param7 AS heap_blks_scanned, + s.param8 AS index_rebuild_count + FROM (pg_stat_get_progress_info('REPACK'::text) s(pid, datid, relid, pa= ram1, param2, param3, param4, param5, param6, param7, param8, param9, param= 10, param11, param12, param13, param14, param15, param16, param17, param18,= param19, param20) + LEFT JOIN pg_database d ON ((s.datid =3D d.oid))); pg_stat_progress_vacuum| SELECT s.pid, s.datid, d.datname, diff --git a/src/test/regress/sql/cluster.sql b/src/test/regress/sql/cluste= r.sql index b7115f86104..c976823a3cb 100644 --- a/src/test/regress/sql/cluster.sql +++ b/src/test/regress/sql/cluster.sql @@ -76,7 +76,6 @@ INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should f= ail'); SELECT conname FROM pg_constraint WHERE conrelid =3D 'clstr_tst'::regclass ORDER BY 1; =20 - SELECT relname, relkind, EXISTS(SELECT 1 FROM pg_class WHERE oid =3D c.reltoastrelid) AS hastoa= st FROM pg_class c WHERE relname LIKE 'clstr_tst%' ORDER BY relname; @@ -229,6 +228,24 @@ SELECT relname, old.level, old.relkind, old.relfilenod= e =3D new.relfilenode FROM o CLUSTER clstrpart; ALTER TABLE clstrpart SET WITHOUT CLUSTER; ALTER TABLE clstrpart CLUSTER ON clstrpart_idx; + +-- Check that REPACK sets new relfilenodes: it should process exactly the = same +-- tables as CLUSTER did. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, = relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class= c ON c.oid=3Dtree.relid ; +REPACK clstrpart USING INDEX clstrpart_idx; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, = relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class= c ON c.oid=3Dtree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode =3D new.relfilenod= e FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname)= ORDER BY relname COLLATE "C"; + +-- And finally the same for REPACK w/o index. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, = relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class= c ON c.oid=3Dtree.relid ; +REPACK clstrpart; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, = relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class= c ON c.oid=3Dtree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode =3D new.relfilenod= e FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname)= ORDER BY relname COLLATE "C"; + DROP TABLE clstrpart; =20 -- Ownership of partitions is checked @@ -313,6 +330,57 @@ EXPLAIN (COSTS OFF) SELECT * FROM clstr_expression WHE= RE -a =3D -3 ORDER BY -a, b; SELECT * FROM clstr_expression WHERE -a =3D -3 ORDER BY -a, b; COMMIT; =20 +---------------------------------------------------------------------- +-- +-- REPACK +-- +---------------------------------------------------------------------- + +-- REPACK handles individual tables identically to CLUSTER, but it's worth +-- checking if it handles table hierarchies identically as well. +REPACK clstr_tst USING INDEX clstr_tst_c; + +-- Verify that inheritance link still works +INSERT INTO clstr_tst_inh VALUES (0, 100, 'in child table 2'); +SELECT a,b,c,substring(d for 30), length(d) from clstr_tst; + +-- Verify that foreign key link still works +INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail'); + +SELECT conname FROM pg_constraint WHERE conrelid =3D 'clstr_tst'::regclass +ORDER BY 1; + +-- Verify partial analyze works +REPACK (ANALYZE) clstr_tst (a); +REPACK (ANALYZE) clstr_tst; +REPACK (VERBOSE) clstr_tst (a); + +-- REPACK w/o argument performs no ordering, so we can only check which ta= bles +-- have the relfilenode changed. +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_old AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); + +SET SESSION AUTHORIZATION regress_clstr_user; +SET client_min_messages =3D ERROR; -- order of "skipping" warnings may va= ry +REPACK; +RESET client_min_messages; + +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_new AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); + +-- Do the actual comparison. Unlike CLUSTER, clstr_3 should have been +-- processed because there is nothing like clustering index here. +SELECT o.relname FROM relnodes_old o +JOIN relnodes_new n ON o.relname =3D n.relname +WHERE o.relfilenode <> n.relfilenode +ORDER BY o.relname; + -- clean up DROP TABLE clustertest; DROP TABLE clstr_1; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs= .list index 1c8610fd46c..da0df6e89d6 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2576,6 +2576,8 @@ ReorderBufferTupleCidKey ReorderBufferUpdateProgressTxnCB ReorderTuple RepOriginId +RepackCommand +RepackStmt ReparameterizeForeignPathByChild_function ReplaceVarsFromTargetList_context ReplaceVarsNoMatchOption --=20 2.47.3 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v31-0002-Refactor-index_concurrently_create_copy-for-use-with.patch ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-01 16:57 SATYANARAYANA NARLAPURAM <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: SATYANARAYANA NARLAPURAM @ 2026-05-01 16:57 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> Hi Daniel, On Thu, Apr 30, 2026 at 8:20 AM Daniel Gustafsson <[email protected]> wrote: > > On 29 Apr 2026, at 15:42, Ayush Tiwari <[email protected]> > wrote: > > > The patchset looks good. > > Thanks for review, I've applied the patchset after some editorializing. > While further testing this feature, I realized that ProcessSingleRelationFork() unconditionally called log_newpage_buffer() for every page of every relation during pg_enable_data_checksums(). This included unlogged relations, which by definition never generate WAL for data changes and are reset to their init fork on any recovery. Guard the log_newpage_buffer() call with RelationNeedsWAL() so that unlogged relations still get their pages dirtied (ensuring the checksum is flushed to disk at the next checkpoint) but do not emit WAL. Attached a patch to address this and added a test for the same. My current test checks if standby has main fork, I could just checked WAL to verify this using pg_waldump. Any other test ideas are welcome. Thanks, Satya Attachments: [application/octet-stream] 0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch (4.8K, ../../CAHg+QDeGrpZbNZdLjd_T4b43xKEEXZN0HGhkFm-1bkBdyzK7AQ@mail.gmail.com/3-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch) download | inline diff: From b54e3dbf7455333a941e2b16a49520b02f7d8b52 Mon Sep 17 00:00:00 2001 From: Satya Narlapuram <[email protected]> Date: Fri, 1 May 2026 15:37:51 +0000 Subject: [PATCH] Skip WAL for unlogged relations during online checksum enable ProcessSingleRelationFork() unconditionally called log_newpage_buffer() for every page of every relation during pg_enable_data_checksums(). This included unlogged relations, which by definition never generate WAL for data changes and are reset to their init fork on any recovery. FPI WAL records are wasteful on the primary (proportional to the total size of all unlogged tables) and cause the standby to pointlessly materialize data files that should not exist there. Guard the log_newpage_buffer() call with RelationNeedsWAL() so that unlogged relations still get their pages dirtied (ensuring the checksum is flushed to disk at the next checkpoint) but do not emit WAL. Add a test to 003_standby_restarts.pl that creates an unlogged table, enables checksums, and verifies via pg_relation_size() on the standby that no data was materialized for the unlogged relation after replay. --- src/backend/postmaster/datachecksum_state.c | 9 +++- .../test_checksums/t/003_standby_restarts.pl | 45 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index d0d6acdd..535da8b0 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -690,11 +690,16 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg * at one point in the past, so only when checksums are first on, then * off, and then turned on again. TODO: investigate if this could be * avoided if the checksum is calculated to be correct and wal_level - * is set to "minimal", + * is set to "minimal". + * + * Unlogged relations don't need WAL since they are reset to their + * init fork on recovery. We still dirty the buffer so that the + * checksum is written to disk at the next checkpoint. */ START_CRIT_SECTION(); MarkBufferDirty(buf); - log_newpage_buffer(buf, false); + if (RelationNeedsWAL(reln)) + log_newpage_buffer(buf, false); END_CRIT_SECTION(); UnlockReleaseBuffer(buf); diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl index 11e15c9d..858329b6 100644 --- a/src/test/modules/test_checksums/t/003_standby_restarts.pl +++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl @@ -115,6 +115,51 @@ $result = $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1"); is($result, "19998", 'ensure we can safely read all data without checksums'); +# --------------------------------------------------------------------------- +# Test that enabling checksums does not emit WAL for unlogged relations. +# Unlogged relations are wiped on recovery, so FPIs for them would be +# pointless and waste WAL traffic / standby I/O. +# + +$node_primary->safe_psql('postgres', + "CREATE UNLOGGED TABLE unlogged_tbl AS SELECT generate_series(1,10000) AS a;"); +$node_primary->wait_for_catchup($node_standby, 'replay', + $node_primary->lsn('insert')); + +# Get the relfilenode and database OID so we can search the WAL for it +my $unlogged_rfn = $node_primary->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE relname = 'unlogged_tbl';"); +my $db_oid = $node_primary->safe_psql('postgres', + "SELECT oid FROM pg_database WHERE datname = 'postgres';"); + +# Verify the standby only has the init fork (no main fork) +my $standby_datadir = $node_standby->data_dir; +ok(!-f "$standby_datadir/base/$db_oid/$unlogged_rfn", + 'standby has no main fork for unlogged table before enable'); + +# Re-enable data checksums +enable_data_checksums($node_primary, wait => 'on'); +wait_for_checksum_state($node_standby, 'on'); + +# After standby replays, the unlogged main file must still not exist. +# If the bug were present, FPI replay would materialize the full table. +$node_primary->wait_for_catchup($node_standby, 'replay', + $node_primary->lsn('insert')); +ok(!-f "$standby_datadir/base/$db_oid/$unlogged_rfn", + 'standby has no main fork for unlogged table after enable'); + +# Verify unlogged relation size is 0 on the standby (main fork missing) +my $standby_size = $node_standby->safe_psql('postgres', + "SELECT pg_relation_size('unlogged_tbl', 'main');"); +is($standby_size, '0', + 'unlogged table has zero size on standby after checksum enable'); + +# Unlogged table should still be readable on primary +$result = $node_primary->safe_psql('postgres', + 'SELECT count(*) FROM unlogged_tbl;'); +is($result, '10000', + 'unlogged table readable on primary after checksum enable'); + $node_standby->stop; $node_primary->stop; done_testing(); -- 2.43.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-01 21:13 Daniel Gustafsson <[email protected]> parent: SATYANARAYANA NARLAPURAM <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Daniel Gustafsson @ 2026-05-01 21:13 UTC (permalink / raw) To: SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> > On 1 May 2026, at 18:57, SATYANARAYANA NARLAPURAM <[email protected]> wrote: > While further testing this feature, I realized that ProcessSingleRelationFork() > unconditionally called log_newpage_buffer() for every page of every relation > during pg_enable_data_checksums(). This included unlogged relations, > which by definition never generate WAL for data changes and are reset to their > init fork on any recovery. > > Guard the log_newpage_buffer() call with RelationNeedsWAL() so that > unlogged relations still get their pages dirtied (ensuring the checksum > is flushed to disk at the next checkpoint) but do not emit WAL. > > Attached a patch to address this and added a test for the same. My current > test checks if standby has main fork, I could just checked WAL to verify this > using pg_waldump. Any other test ideas are welcome. Thanks for the report, I agree that this is an oversight that should be fixed. Your patch looks good on first glance, I am travelling till Sunday evening so will take another look when back in the office and will apply it then. Thanks! -- Daniel Gustafsson ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-04 13:16 Tomas Vondra <[email protected]> parent: Daniel Gustafsson <[email protected]> 0 siblings, 3 replies; 24+ messages in thread From: Tomas Vondra @ 2026-05-04 13:16 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> Hi, Thanks for getting this feature pushed, and for resolving the failures reported since the feature freeze. I consider this to be an important improvement, not just for the feature itself, but also because of all the useful infrastructure it added. Attached is a refined version of the TAP tests already posted by Daniel some time ago [1]. Unfortunately, that .txt did not apply cleanly for some reason, so here's a better version. I found these tests quite useful when reasoning about how the patch behaves in concurrent environment (e.g. with multiple sessions triggering checksum enable/disable, or with a checkpoint, crashes, etc). At this point all the tests pass, but there are a couple cases with correct but slightly surprising behavior, worth discussing. Which is what this e-mail is going to be about. I'll explain what the TAP tests aim to do first, and then discuss the slightly surprising behavior. It's not meant for inclusion into PG19, at least not in this shape - I wrote those TAP tests while investigating some of the earlier failures and/or when wondering about behavior in various situations (sequence of concurrent steps, race conditions, ...). So it's more of an exhaustive, and the tests are somewhat redundant (N+1 is often just (N + some small tweak)). I can imagine distilling it into a tiny subset, and adding that. But that's up to discussion. But that's for later. Let me briefly explain what the various TAP tests aim to do. From the very beginning, my main concern regarding this patch was race conditions when updating the shared state about effective data_checksum_version. Because the state is effectively split into about three or four places: * LocalDataChecksumVersion (local cache) * XLogCtl->data_checksum_version (XLogCtl->info_lck) * ControlFile->data_checksum_version (ControlFileLock) * state in control file on disk These pieces are protected by different locks, the protocol for updating and/or reading the various flags is not trivial (and some of the fixed issues were due to ControlFile->data_checksum_version being updated from a place that shouldn't have). So the primary goal of the TAP tests was to check for race conditions by leveraging injection points to step through concurrent processes in a deterministic way. The first couple patches (0001-0004) add debug logging and injection points into a lot of places. And by "a lot" I mean ~80 new injection points, which is about the number of injection points we have in master now. Anyway, this allows stepping through concurrent checksum changes, and also checksum change vs. checkpointer. Then come the actual TAP tests: 1) 0005-TAP-10-concurrent-checksum-changes.patch Two concurrent checksum changes. The first one gets paused at an injection point, then the second one gets initiated. 2) 0006-TAP-11-concurrency-with-checkpoints.patch A checksum change + checkpoint. The change gets paused at an injection point, a synchronous checkpoint is performed. 3) 0007-TAP-12-crashes-at-injection-points.patch Similar to 0006, but with a crash + recovery. A checksum change gets paused at an injection point, a synchronous checkpoint is performed. The changes gets wpken up and either completes, or pauses on a different injection point. A restart/crash happens. 4) 0008-TAP-13-concurrency-with-checkpoint-REDO.patch Similar to 0007, but the checkpoint is not synchronous - happens in the background, so that the TAP can step through both sides and interleave them in an arbitrary way. This matters because the checksum change updates the different state pieces (XlogCtl/ControlFile), while the checkpointer reads them to record initial state for REDO etc. 5) 0009-TAP-14-checkpoints-with-crashes.patch Similar to 0008, except that the steps are more fine grained, and focused on two particular cases with surprisingly different final state. AFAIK everything works as expected, except for two cases in the "TAP 012" test. One for the "enabling" direction, one for the "disabling" direction. I'm going to discuss the "enabling" direction, I believe the other case is just a mirror with the same root cause. The TAP 012 tests checksum change with a concurrent checkpoint, followed by a crash, and tests the final state. It pauses the change at an injection point, does a checkpoint, proceeds to the next injection point, crashes and does recovery. The expectation is that the final state "flips" at some injection point, once it gets further enough, and stays there. But what actually happens is this: a) test_checksum_transition( 'disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'datachecksums-enable-checksums-start', 'off'); b) test_checksum_transition( 'disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on'); c) test_checksum_transition( 'disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogs', 'datachecksums-enable-checksums-after-xlogctl', 'off'); This says that if the checkpoint happens after 'datachecksums-enable-inprogress-checksums-end' or after 'datachecksums-enable-checksums-after-xlog', we end up with 'off' (i.e. enabling checksums fails). But if the checkpoint happens after 'datachecksums-enable-checksums-start', we end up with "on" (after recovery). This is a bit surprising, because that injection point is before 'datachecksums-enable-checksums-after-xlog'. So the enabling process gets further and further, but the final state flips off -> on -> off, contradicting the expectation that it changes once. I haven't quite wrapped my head around it yet, but my understanding is this is due to a race condition between the checksum launcher (writing XLOG2_CHECKSUMS and updating the shmem state), and the checkpointer (reading the shmem state and generating REDO). The launcher does this sequence of steps: 1) write XLOG2_CHECKSUMS with new state 2) update XLogCtl->data_checksum_version 3) update ControlFile->data_checksum_version 4) UpdateControlFile() 5) emits barrier while the checkpointer (CreateCheckPoint) does this: A) read XLogCtl->data_checksum_version (while holding insert locks) B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version) C) UpdateControlFile() The outcome depends on how exactly these two sequences interleave. For example, this can happen: 1) write XLOG2_CHECKSUMS with new state A) read XLogCtl->data_checksum_version (while holding insert locks) B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version) C) UpdateControlFile() 2) update XLogCtl->data_checksum_version 3) update ControlFile->data_checksum_version 4) UpdateControlFile() 5) emits barrier Which means the XLOG_CHECKPOINT_REDO will be after XLOG2_CHECKSUMS (and so redo won't see it), but the checkpoint will still get the old checksum state from XLogCtl. And so the outcome is "off", per case (c). But it can also happen what case (b) does: A) read XLogCtl->data_checksum_version (while holding insert locks) B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version) C) UpdateControlFile() 1) write XLOG2_CHECKSUMS with new state 2) update XLogCtl->data_checksum_version 3) update ControlFile->data_checksum_version 4) UpdateControlFile() 5) emits barrier In which case the REDO will have the old state, but the recovery will read the XLOG2_CHECKSUMS, and so end up with "on". This is the root cause of the surprising behavior in TAP 012, I think. I attempted to trigger these race conditions in TAP 013, but without much success. In the end I realized it probably needs more control, waiting for the other process to hit the next injection point before unpausing the current one. TAP 014 does that, and it shows that with the right interleaving of steps the (c) case can end up with both "on" and "off" final state. As I said, I don't claim I fully understand this yet. But I wouldn't call this "bug" - AFAICS it won't produce an incorrect final state (I haven't seen any such cases). Still, I wonder if there's a potential issue I failed to notice. The other question I had when looking at this (concurrency with checkpoints) is what we get by doing MyProc->delayChkptFlags |= DELAY_CHKPT_START; whenever updating the state in SetDataChecksums... functions. Because the only thing that guarantees is the updates happen on one side of the checkpoint record. What does that give us, actually? It does not seem to prevent this surprising behavior, and it does not say the XLOG2_CHECKSUMS happens before/after the XLOG_CHECKPOINT_REDO. regards [1] https://www.postgresql.org/message-id/9197F930-DDEB-4CAC-82A2-16FEC715CCE8%40yesql.se -- Tomas Vondra Attachments: [text/x-patch] 0001-debug-checkpoint-logging.patch (1.2K, ../../[email protected]/2-0001-debug-checkpoint-logging.patch) download | inline diff: From 696077acf842ce1de0bba320b10c18e5c22870e1 Mon Sep 17 00:00:00 2001 From: Tomas Vondra <[email protected]> Date: Mon, 13 Apr 2026 14:36:43 +0200 Subject: [PATCH 01/13] debug: checkpoint logging --- src/backend/access/transam/xlog.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 18d5dee06e0..94b7afeb6c7 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -7568,6 +7568,9 @@ CreateCheckPoint(int flags) */ WALInsertLockRelease(); + elog(LOG, "CreateCheckPoint start redo %X/%X checksums %d", + LSN_FORMAT_ARGS(RedoRecPtr), checkPoint.dataChecksumState); + /* * If this is an online checkpoint, we have not yet determined the redo * point. We do so now by inserting the special XLOG_CHECKPOINT_REDO @@ -7811,6 +7814,10 @@ CreateCheckPoint(int flags) */ END_CRIT_SECTION(); + elog(LOG, "CreateCheckPoint end lsn %X/%X redo %X/%X checksums %d", + LSN_FORMAT_ARGS(recptr), LSN_FORMAT_ARGS(RedoRecPtr), + checkPoint.dataChecksumState); + /* * WAL summaries end when the next XLOG_CHECKPOINT_REDO or * XLOG_CHECKPOINT_SHUTDOWN record is reached. This is the first point -- 2.54.0 [text/x-patch] 0002-debug-state-transitions.patch (14.2K, ../../[email protected]/3-0002-debug-state-transitions.patch) download | inline diff: From 15c2237b02cbf93558f6f8110f046acce976e0e4 Mon Sep 17 00:00:00 2001 From: Tomas Vondra <[email protected]> Date: Wed, 8 Apr 2026 22:07:21 +0200 Subject: [PATCH 02/13] debug: state transitions --- src/backend/access/transam/xlog.c | 125 ++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 94b7afeb6c7..72110d92c8b 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -4288,12 +4288,14 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version) ControlFile->wal_log_hints = wal_log_hints; ControlFile->track_commit_timestamp = track_commit_timestamp; ControlFile->data_checksum_version = data_checksum_version; + elog(LOG, "InitControlFile ControlFile->data_checksum_version = %u", data_checksum_version); /* * Set the data_checksum_version value into XLogCtl, which is where all * processes get the current value from. */ XLogCtl->data_checksum_version = data_checksum_version; + elog(LOG, "InitControlFile XLogCtl->data_checksum_version = %u", data_checksum_version); } static void @@ -4753,6 +4755,9 @@ void SetDataChecksumsOnInProgress(void) { uint64 barrier; + uint32 data_checksum_version; + + elog(LOG, "SetDataChecksumsOnInProgress / start"); /* * The state transition is performed in a critical section with @@ -4764,20 +4769,32 @@ SetDataChecksumsOnInProgress(void) XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON); SpinLockAcquire(&XLogCtl->info_lck); + data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON; SpinLockRelease(&XLogCtl->info_lck); + elog(LOG, "SetDataChecksumsOnInProgress XLogCtl->data_checksum_version %u => %u", + data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON); + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + + elog(LOG, "SetDataChecksumsOnInProgress ControlFile->data_checksum_version %u => %u", + ControlFile->data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON); + ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON; UpdateControlFile(); LWLockRelease(ControlFileLock); + elog(LOG, "SetDataChecksumsOnInProgress / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)"); barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON); MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; END_CRIT_SECTION(); + elog(LOG, "SetDataChecksumsOnInProgress / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)"); WaitForProcSignalBarrier(barrier); + + elog(LOG, "SetDataChecksumsOnInProgress / end"); } /* @@ -4806,6 +4823,9 @@ void SetDataChecksumsOn(void) { uint64 barrier; + uint32 data_checksum_version; + + elog(LOG, "SetDataChecksumsOn / start"); SpinLockAcquire(&XLogCtl->info_lck); @@ -4833,25 +4853,39 @@ SetDataChecksumsOn(void) XLogChecksums(PG_DATA_CHECKSUM_VERSION); SpinLockAcquire(&XLogCtl->info_lck); + data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION; SpinLockRelease(&XLogCtl->info_lck); + elog(LOG, "SetDataChecksumsOn / XLogCtl->data_checksum_version %u => %u", + data_checksum_version, PG_DATA_CHECKSUM_VERSION); + /* * Update the controlfile before waiting since if we have an immediate * shutdown while waiting we want to come back up with checksums enabled. */ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + + elog(LOG, "SetDataChecksumsOn / ControlFile->data_checksum_version %u => %u", + ControlFile->data_checksum_version, PG_DATA_CHECKSUM_VERSION); + ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION; UpdateControlFile(); LWLockRelease(ControlFileLock); + elog(LOG, "SetDataChecksumsOn / EmitProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)"); barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON); MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; END_CRIT_SECTION(); + elog(LOG, "SetDataChecksumsOn / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)"); RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); + + elog(LOG, "SetDataChecksumsOn / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)"); WaitForProcSignalBarrier(barrier); + + elog(LOG, "SetDataChecksumsOn / end"); } /* @@ -4871,6 +4905,9 @@ void SetDataChecksumsOff(void) { uint64 barrier; + uint32 data_checksum_version; + + elog(LOG, "SetDataChecksumsOff / start"); SpinLockAcquire(&XLogCtl->info_lck); @@ -4899,20 +4936,32 @@ SetDataChecksumsOff(void) XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF); SpinLockAcquire(&XLogCtl->info_lck); + data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF; SpinLockRelease(&XLogCtl->info_lck); + elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u", + data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF); + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + + elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u", + ControlFile->data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF); + ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF; UpdateControlFile(); LWLockRelease(ControlFileLock); + elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)"); barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF); MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; END_CRIT_SECTION(); + elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)"); RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); + + elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)"); WaitForProcSignalBarrier(barrier); /* @@ -4937,21 +4986,35 @@ SetDataChecksumsOff(void) XLogChecksums(PG_DATA_CHECKSUM_OFF); SpinLockAcquire(&XLogCtl->info_lck); + data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF; SpinLockRelease(&XLogCtl->info_lck); + elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u", + data_checksum_version, PG_DATA_CHECKSUM_OFF); + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + + elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u", + ControlFile->data_checksum_version, PG_DATA_CHECKSUM_OFF); + ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF; UpdateControlFile(); LWLockRelease(ControlFileLock); + elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)"); barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF); MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; END_CRIT_SECTION(); + elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)"); RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); + + elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)"); WaitForProcSignalBarrier(barrier); + + elog(LOG, "SetDataChecksumsOff / end"); } /* @@ -4966,9 +5029,14 @@ void InitLocalDataChecksumState(void) { Assert(InterruptHoldoffCount > 0); + + elog(LOG, "InitLocalDataChecksumState start %d", LocalDataChecksumState); + SpinLockAcquire(&XLogCtl->info_lck); SetLocalDataChecksumState(XLogCtl->data_checksum_version); SpinLockRelease(&XLogCtl->info_lck); + + elog(LOG, "InitLocalDataChecksumState end %d", LocalDataChecksumState); } void @@ -5278,7 +5346,9 @@ LocalProcessControlFile(bool reset) LocalControlFile = palloc_object(ControlFileData); ControlFile = LocalControlFile; ReadControlFile(); + elog(LOG, "LocalProcessControlFile start %d", LocalDataChecksumState); SetLocalDataChecksumState(ControlFile->data_checksum_version); + elog(LOG, "LocalProcessControlFile end %d", LocalDataChecksumState); } /* @@ -5430,9 +5500,15 @@ XLOGShmemInit(void *arg) XLogCtl->InstallXLogFileSegmentActive = false; XLogCtl->WalWriterSleeping = false; + elog(LOG, "XLOGShmemInit / XLogCtl->data_checksum_version %u => %u", + XLogCtl->data_checksum_version, ControlFile->data_checksum_version); + /* Use the checksum info from control file */ XLogCtl->data_checksum_version = ControlFile->data_checksum_version; + + elog(LOG, "XLOGShmemInit start %d", LocalDataChecksumState); SetLocalDataChecksumState(XLogCtl->data_checksum_version); + elog(LOG, "XLOGShmemInit end %d", LocalDataChecksumState); SpinLockInit(&XLogCtl->Insert.insertpos_lck); SpinLockInit(&XLogCtl->info_lck); @@ -6607,13 +6683,22 @@ StartupXLOG(void) */ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON) { + uint32 data_checksum_version; XLogChecksums(PG_DATA_CHECKSUM_OFF); + elog(LOG, "StartupXLOG start %d", LocalDataChecksumState); + SpinLockAcquire(&XLogCtl->info_lck); + data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF; SetLocalDataChecksumState(XLogCtl->data_checksum_version); SpinLockRelease(&XLogCtl->info_lck); + elog(LOG, "StartupXLOG XLogCtl->data_checksum_version %u => %u", + data_checksum_version, PG_DATA_CHECKSUM_OFF); + + elog(LOG, "StartupXLOG end %d", LocalDataChecksumState); + ereport(WARNING, errmsg("enabling data checksums was interrupted"), errhint("Data checksum processing must be manually restarted for checksums to be enabled")); @@ -6627,12 +6712,21 @@ StartupXLOG(void) */ if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF) { + uint32 data_checksum_version; XLogChecksums(PG_DATA_CHECKSUM_OFF); + elog(LOG, "StartupXLOG start %d", LocalDataChecksumState); + SpinLockAcquire(&XLogCtl->info_lck); + data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF; SetLocalDataChecksumState(XLogCtl->data_checksum_version); SpinLockRelease(&XLogCtl->info_lck); + + elog(LOG, "StartupXLOG XLogCtl->data_checksum_version %u => %u", + data_checksum_version, PG_DATA_CHECKSUM_OFF); + + elog(LOG, "StartupXLOG end %d", LocalDataChecksumState); } /* @@ -7917,6 +8011,8 @@ CreateEndOfRecoveryRecord(void) { xl_end_of_recovery xlrec; XLogRecPtr recptr; + uint32 old_data_checksum_version; + uint32 new_data_checksum_version; /* sanity check */ if (!RecoveryInProgress()) @@ -7948,9 +8044,14 @@ CreateEndOfRecoveryRecord(void) /* start with the latest checksum version (as of the end of recovery) */ SpinLockAcquire(&XLogCtl->info_lck); + old_data_checksum_version = ControlFile->data_checksum_version; + new_data_checksum_version = XLogCtl->data_checksum_version; ControlFile->data_checksum_version = XLogCtl->data_checksum_version; SpinLockRelease(&XLogCtl->info_lck); + elog(LOG, "CreateEndOfRecoveryRecord ControlFile->data_checksum_version %u => %u", + old_data_checksum_version, new_data_checksum_version); + UpdateControlFile(); LWLockRelease(ControlFileLock); @@ -8293,6 +8394,9 @@ CreateRestartPoint(int flags) ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY; } + elog(LOG, "CreateRestartPoint ControlFile->data_checksum_version %u => %u", + ControlFile->data_checksum_version, lastCheckPoint.dataChecksumState); + /* we shall start with the latest checksum version */ ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState; @@ -8750,6 +8854,8 @@ XLogChecksums(uint32 new_type) recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS); XLogFlush(recptr); + + elog(LOG, "XLogChecksums %d %X/%X", new_type, LSN_FORMAT_ARGS(recptr)); } /* @@ -8862,6 +8968,7 @@ xlog_redo(XLogReaderState *record) { CheckPoint checkPoint; TimeLineID replayTLI; + uint32 data_checksum_version; memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint)); /* In a SHUTDOWN checkpoint, believe the counters exactly */ @@ -8936,6 +9043,10 @@ xlog_redo(XLogReaderState *record) /* ControlFile->checkPointCopy always tracks the latest ckpt XID */ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + + elog(LOG, "xlog_redo / ControlFile->data_checksum_version %u => %u", + ControlFile->data_checksum_version, checkPoint.dataChecksumState); + ControlFile->checkPointCopy.nextXid = checkPoint.nextXid; ControlFile->data_checksum_version = checkPoint.dataChecksumState; @@ -9178,16 +9289,21 @@ xlog_redo(XLogReaderState *record) { xl_checkpoint_redo redo_rec; bool new_state = false; + uint32 data_checksum_version; memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo)); SpinLockAcquire(&XLogCtl->info_lck); + data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = redo_rec.data_checksum_version; SetLocalDataChecksumState(redo_rec.data_checksum_version); if (redo_rec.data_checksum_version != ControlFile->data_checksum_version) new_state = true; SpinLockRelease(&XLogCtl->info_lck); + elog(LOG, "xlog_redo / XLogCtl->data_checksum_version %u => %u", + data_checksum_version, redo_rec.data_checksum_version); + if (new_state) EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version); } @@ -9250,18 +9366,27 @@ xlog2_redo(XLogReaderState *record) if (info == XLOG2_CHECKSUMS) { xl_checksum_state state; + uint32 data_checksum_version; memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state)); SpinLockAcquire(&XLogCtl->info_lck); + data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = state.new_checksum_state; SpinLockRelease(&XLogCtl->info_lck); + elog(LOG, "xlog2_redo / XLogCtl->data_checksum_version %u => %u", + data_checksum_version, state.new_checksum_state); + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + data_checksum_version = ControlFile->data_checksum_version; ControlFile->data_checksum_version = state.new_checksum_state; UpdateControlFile(); LWLockRelease(ControlFileLock); + elog(LOG, "xlog2_redo / ControlFile->data_checksum_version %u => %u", + data_checksum_version, state.new_checksum_state); + /* * Block on a procsignalbarrier to await all processes having seen the * change to checksum status. Once the barrier has been passed we can -- 2.54.0 [text/x-patch] 0003-debug-checksums-injection-points.patch (9.6K, ../../[email protected]/4-0003-debug-checksums-injection-points.patch) download | inline diff: From 1e58b18a70502339c5608ba352f6e351f6114359 Mon Sep 17 00:00:00 2001 From: test <test> Date: Fri, 1 May 2026 22:14:18 +0200 Subject: [PATCH 03/13] debug: checksums injection points --- src/backend/access/transam/xlog.c | 74 ++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 72110d92c8b..8dd003d25e2 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -4759,6 +4759,13 @@ SetDataChecksumsOnInProgress(void) elog(LOG, "SetDataChecksumsOnInProgress / start"); + INJECTION_POINT("datachecksums-enable-inprogress-checksums-start", NULL); + + /* load before critical section */ + INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlog"); + INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlogctl"); + INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-controlfile"); + /* * The state transition is performed in a critical section with * checkpoints held off to provide crash safety. @@ -4768,6 +4775,8 @@ SetDataChecksumsOnInProgress(void) XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON); + INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlog", NULL); + SpinLockAcquire(&XLogCtl->info_lck); data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON; @@ -4776,6 +4785,8 @@ SetDataChecksumsOnInProgress(void) elog(LOG, "SetDataChecksumsOnInProgress XLogCtl->data_checksum_version %u => %u", data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON); + INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlogctl", NULL); + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); elog(LOG, "SetDataChecksumsOnInProgress ControlFile->data_checksum_version %u => %u", @@ -4785,15 +4796,21 @@ SetDataChecksumsOnInProgress(void) UpdateControlFile(); LWLockRelease(ControlFileLock); + INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-controlfile", NULL); + elog(LOG, "SetDataChecksumsOnInProgress / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)"); barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON); MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; END_CRIT_SECTION(); + INJECTION_POINT("datachecksums-enable-inprogress-checksums-before-barrier-wait", NULL); + elog(LOG, "SetDataChecksumsOnInProgress / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)"); WaitForProcSignalBarrier(barrier); + INJECTION_POINT("datachecksums-enable-inprogress-checksums-end", NULL); + elog(LOG, "SetDataChecksumsOnInProgress / end"); } @@ -4846,12 +4863,20 @@ SetDataChecksumsOn(void) SpinLockRelease(&XLogCtl->info_lck); - INJECTION_POINT("datachecksums-enable-checksums-delay", NULL); + INJECTION_POINT("datachecksums-enable-checksums-start", NULL); + + /* load before critical section */ + INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlog"); + INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlogctl"); + INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-controlfile"); + START_CRIT_SECTION(); MyProc->delayChkptFlags |= DELAY_CHKPT_START; XLogChecksums(PG_DATA_CHECKSUM_VERSION); + INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlog", NULL); + SpinLockAcquire(&XLogCtl->info_lck); data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION; @@ -4860,6 +4885,8 @@ SetDataChecksumsOn(void) elog(LOG, "SetDataChecksumsOn / XLogCtl->data_checksum_version %u => %u", data_checksum_version, PG_DATA_CHECKSUM_VERSION); + INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlogctl", NULL); + /* * Update the controlfile before waiting since if we have an immediate * shutdown while waiting we want to come back up with checksums enabled. @@ -4873,18 +4900,26 @@ SetDataChecksumsOn(void) UpdateControlFile(); LWLockRelease(ControlFileLock); + INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-controlfile", NULL); + elog(LOG, "SetDataChecksumsOn / EmitProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)"); barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON); MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; END_CRIT_SECTION(); + INJECTION_POINT("datachecksums-enable-checksums-before-checkpoint", NULL); + elog(LOG, "SetDataChecksumsOn / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)"); RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); + INJECTION_POINT("datachecksums-enable-checksums-before-barrier-wait", NULL); + elog(LOG, "SetDataChecksumsOn / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)"); WaitForProcSignalBarrier(barrier); + INJECTION_POINT("datachecksums-enable-checksums-end", NULL); + elog(LOG, "SetDataChecksumsOn / end"); } @@ -4930,11 +4965,20 @@ SetDataChecksumsOff(void) { SpinLockRelease(&XLogCtl->info_lck); + INJECTION_POINT("datachecksums-disable-inprogress-checksums-start", NULL); + + /* load before critical section */ + INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlog"); + INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlogctl"); + INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-controlfile"); + START_CRIT_SECTION(); MyProc->delayChkptFlags |= DELAY_CHKPT_START; XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF); + INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlog", NULL); + SpinLockAcquire(&XLogCtl->info_lck); data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF; @@ -4943,6 +4987,8 @@ SetDataChecksumsOff(void) elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u", data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF); + INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlogctl", NULL); + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u", @@ -4952,15 +4998,21 @@ SetDataChecksumsOff(void) UpdateControlFile(); LWLockRelease(ControlFileLock); + INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-controlfile", NULL); + elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)"); barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF); MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; END_CRIT_SECTION(); + INJECTION_POINT("datachecksums-disable-inprogress-checksums-before-checkpoint", NULL); + elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)"); RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); + INJECTION_POINT("datachecksums-disable-inprogress-checksums-before-barrier-wait", NULL); + elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)"); WaitForProcSignalBarrier(barrier); @@ -4979,12 +5031,21 @@ SetDataChecksumsOff(void) SpinLockRelease(&XLogCtl->info_lck); } + INJECTION_POINT("datachecksums-disable-checksums-start", NULL); + + /* load before critical section */ + INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlog"); + INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlogctl"); + INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-controlfile"); + START_CRIT_SECTION(); /* Ensure that we don't incur a checkpoint during disabling checksums */ MyProc->delayChkptFlags |= DELAY_CHKPT_START; XLogChecksums(PG_DATA_CHECKSUM_OFF); + INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlog", NULL); + SpinLockAcquire(&XLogCtl->info_lck); data_checksum_version = XLogCtl->data_checksum_version; XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF; @@ -4993,6 +5054,8 @@ SetDataChecksumsOff(void) elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u", data_checksum_version, PG_DATA_CHECKSUM_OFF); + INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlogctl", NULL); + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u", @@ -5002,18 +5065,26 @@ SetDataChecksumsOff(void) UpdateControlFile(); LWLockRelease(ControlFileLock); + INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-controlfile", NULL); + elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)"); barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF); MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; END_CRIT_SECTION(); + INJECTION_POINT("datachecksums-disable-checksums-before-checkpoint", NULL); + elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)"); RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); + INJECTION_POINT("datachecksums-disable-checksums-before-barrier-wait", NULL); + elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)"); WaitForProcSignalBarrier(barrier); + INJECTION_POINT("datachecksums-disable-checksums-end", NULL); + elog(LOG, "SetDataChecksumsOff / end"); } @@ -8968,7 +9039,6 @@ xlog_redo(XLogReaderState *record) { CheckPoint checkPoint; TimeLineID replayTLI; - uint32 data_checksum_version; memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint)); /* In a SHUTDOWN checkpoint, believe the counters exactly */ -- 2.54.0 [text/x-patch] 0004-debug-checkpointer-injection-points.patch (6.0K, ../../[email protected]/5-0004-debug-checkpointer-injection-points.patch) download | inline diff: From 0c3dd8ac1df7fd1686374555eb766e4517c4c1a8 Mon Sep 17 00:00:00 2001 From: test <test> Date: Fri, 1 May 2026 22:15:52 +0200 Subject: [PATCH 04/13] debug: checkpointer injection points --- src/backend/access/transam/xlog.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 8dd003d25e2..7c4e412093c 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -4762,6 +4762,7 @@ SetDataChecksumsOnInProgress(void) INJECTION_POINT("datachecksums-enable-inprogress-checksums-start", NULL); /* load before critical section */ + INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-before-xlog"); INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlog"); INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlogctl"); INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-controlfile"); @@ -4773,6 +4774,8 @@ SetDataChecksumsOnInProgress(void) START_CRIT_SECTION(); MyProc->delayChkptFlags |= DELAY_CHKPT_START; + INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-before-xlog", NULL); + XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON); INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlog", NULL); @@ -4866,6 +4869,7 @@ SetDataChecksumsOn(void) INJECTION_POINT("datachecksums-enable-checksums-start", NULL); /* load before critical section */ + INJECTION_POINT_LOAD("datachecksums-enable-checksums-before-xlog"); INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlog"); INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlogctl"); INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-controlfile"); @@ -4873,6 +4877,8 @@ SetDataChecksumsOn(void) START_CRIT_SECTION(); MyProc->delayChkptFlags |= DELAY_CHKPT_START; + INJECTION_POINT_CACHED("datachecksums-enable-checksums-before-xlog", NULL); + XLogChecksums(PG_DATA_CHECKSUM_VERSION); INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlog", NULL); @@ -4968,6 +4974,7 @@ SetDataChecksumsOff(void) INJECTION_POINT("datachecksums-disable-inprogress-checksums-start", NULL); /* load before critical section */ + INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-before-xlog"); INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlog"); INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlogctl"); INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-controlfile"); @@ -4975,6 +4982,8 @@ SetDataChecksumsOff(void) START_CRIT_SECTION(); MyProc->delayChkptFlags |= DELAY_CHKPT_START; + INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-before-xlog", NULL); + XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF); INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlog", NULL); @@ -5034,6 +5043,7 @@ SetDataChecksumsOff(void) INJECTION_POINT("datachecksums-disable-checksums-start", NULL); /* load before critical section */ + INJECTION_POINT_LOAD("datachecksums-disable-checksums-before-xlog"); INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlog"); INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlogctl"); INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-controlfile"); @@ -5042,6 +5052,8 @@ SetDataChecksumsOff(void) /* Ensure that we don't incur a checkpoint during disabling checksums */ MyProc->delayChkptFlags |= DELAY_CHKPT_START; + INJECTION_POINT_CACHED("datachecksums-disable-checksums-before-xlog", NULL); + XLogChecksums(PG_DATA_CHECKSUM_OFF); INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlog", NULL); @@ -7611,6 +7623,12 @@ CreateCheckPoint(int flags) /* Run these points outside the critical section. */ INJECTION_POINT("create-checkpoint-initial", NULL); INJECTION_POINT_LOAD("create-checkpoint-run"); + INJECTION_POINT_LOAD("checkpoint-before-redo"); + INJECTION_POINT_LOAD("checkpoint-before-xlogctl-checksums"); + INJECTION_POINT_LOAD("checkpoint-after-xlogctl-checksums"); + INJECTION_POINT_LOAD("checkpoint-before-redo-checksums"); + INJECTION_POINT_LOAD("checkpoint-before-redo-wal"); + INJECTION_POINT_LOAD("checkpoint-after-redo-wal"); /* * Use a critical section to force system panic if we have trouble. @@ -7676,6 +7694,8 @@ CreateCheckPoint(int flags) else checkPoint.PrevTimeLineID = checkPoint.ThisTimeLineID; + INJECTION_POINT_CACHED("checkpoint-before-redo", NULL); + /* * We must block concurrent insertions while examining insert state. */ @@ -7684,6 +7704,8 @@ CreateCheckPoint(int flags) checkPoint.fullPageWrites = Insert->fullPageWrites; checkPoint.wal_level = wal_level; + INJECTION_POINT_CACHED("checkpoint-before-xlogctl-checksums", NULL); + /* * Get the current data_checksum_version value from xlogctl, valid at the * time of the checkpoint. @@ -7692,6 +7714,8 @@ CreateCheckPoint(int flags) checkPoint.dataChecksumState = XLogCtl->data_checksum_version; SpinLockRelease(&XLogCtl->info_lck); + INJECTION_POINT_CACHED("checkpoint-after-xlogctl-checksums", NULL); + if (shutdown) { XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos); @@ -7749,6 +7773,8 @@ CreateCheckPoint(int flags) { xl_checkpoint_redo redo_rec; + INJECTION_POINT_CACHED("checkpoint-before-redo-checksums", NULL); + WALInsertLockAcquire(); redo_rec.wal_level = wal_level; SpinLockAcquire(&XLogCtl->info_lck); @@ -7756,6 +7782,8 @@ CreateCheckPoint(int flags) SpinLockRelease(&XLogCtl->info_lck); WALInsertLockRelease(); + INJECTION_POINT_CACHED("checkpoint-before-redo-wal", NULL); + /* Include WAL level in record for WAL summarizer's benefit. */ XLogBeginInsert(); XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo)); @@ -7768,6 +7796,8 @@ CreateCheckPoint(int flags) * checkpoint is complete. */ checkPoint.redo = RedoRecPtr; + + INJECTION_POINT_CACHED("checkpoint-after-redo-wal", NULL); } /* Update the info_lck-protected copy of RedoRecPtr as well */ -- 2.54.0 [text/x-patch] 0005-TAP-10-concurrent-checksum-changes.patch (16.0K, ../../[email protected]/6-0005-TAP-10-concurrent-checksum-changes.patch) download | inline diff: From 24ab81ae666ed9de683091d6feecbf80b2dd5bb1 Mon Sep 17 00:00:00 2001 From: test <test> Date: Fri, 1 May 2026 22:14:49 +0200 Subject: [PATCH 05/13] TAP 10: concurrent checksum changes --- .../t/010_concurrent_changes.pl | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 src/test/modules/test_checksums/t/010_concurrent_changes.pl diff --git a/src/test/modules/test_checksums/t/010_concurrent_changes.pl b/src/test/modules/test_checksums/t/010_concurrent_changes.pl new file mode 100644 index 00000000000..1b17b496033 --- /dev/null +++ b/src/test/modules/test_checksums/t/010_concurrent_changes.pl @@ -0,0 +1,296 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster with +# injection point tests injecting failures into the processing + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# This test suite is expensive, or very expensive, to execute. There are two +# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite +# an "checksum_extended" for the full suite. The full suite can run for hours +# on slow or constrained systems. +my $extended = undef; +if ($ENV{PG_TEST_EXTRA}) +{ + $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/); + plan skip_all => 'Expensive data checksums test disabled' + unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/); +} + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +# --------------------------------------------------------------------------- +# Test cluster setup +# + +# Initiate testcluster +my $node = PostgreSQL::Test::Cluster->new('injection_node'); +$node->init(no_data_checksums => 1); +$node->start; + +# Set up test environment +$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +my $pgbench = undef; +my $scalefactor = ($extended ? 10 : 1); +my $node_loglocation = 0; + +$node->command_ok( + [ + 'pgbench', '-p', $node->port, '-i', + '-s', $scalefactor, '-q', 'postgres' + ]); + +# Start a pgbench run in the background against the server specified via the +# port passed as parameter. +sub background_rw_pgbench +{ + my $port = shift; + + # If a previous pgbench is still running, start by shutting it down. + $pgbench->finish if $pgbench; + + my $clients = 1; + my $runtime = 2; + + if ($extended) + { + # Randomize the number of pgbench clients a bit (range 1-16) + $clients = 1 + int(rand(15)); + $runtime = 600; + } + my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients); + + # Randomize whether we spawn connections or not + push(@cmd, '-C') if ($extended && cointoss); + # Finally add the database name to use + push(@cmd, 'postgres'); + + $pgbench = IPC::Run::start( + \@cmd, + '<' => '/dev/null', + '>' => '/dev/null', + '2>' => '/dev/null', + IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default)); +} + +sub attach_injection_point +{ + my ($node, $point) = @_; + note('attaching injection point: ' . $point); + $node->safe_psql('postgres', + "SELECT injection_points_attach('" . $point . "','wait');" + ); +} + +sub wait_injection_point +{ + my ($node, $point) = @_; + + note("waiting for the injection point to be hit"); + $node->poll_query_until( + 'postgres', + "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'", + '1'); +} + +sub wakeup_injection_point +{ + my ($node, $point) = @_; + + note('waking the init injection point: ' . $point); + $node->safe_psql('postgres', + "SELECT injection_points_wakeup('" . $point . "');" + ); + + note("detaching the init injection point"); + $node->safe_psql('postgres', + "SELECT injection_points_detach('" . $point . "');"); +} + +# Test behavior with two concurrent checksum transitions. +# +# The cluster is initialized into checksum state $start (enabled/disabled), +# and two changes are initiated in a controled way. The $first change gets +# paused at a selected injection point $point, at which point $second change +# gets initiated. Once the checksum resolves the cluster state, the state +# is compared to the expected $final state. +# +# arguments: +# +# - start - starting checksum state (enabled/disabled) +# - first - first checksum change +# - second - second checksum change +# - init - initial injection point (see comment later) +# - point - injection point the first change should wait on +# - final - expected checksum state at the end +# +# The injection point is triggered only by the datachecksum launcher, and +# there can be only one such process. So there's no risk of hitting the +# injection point by both changes. +# +# XXX Some of the injection points are in a critical section, which does +# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles +# just private memory allocation, but 'wait' requires a shmem allocation. +# To deal with that, we setup an injection point $init outside a critical +# section to initialize the shmem stuff needed by 'wait'. +# +# XXX The first change gets "unpaused" after the second checksum change +# gets initiated, but maybe that's not a sufficiently deterministic? +# +# XXX Should we detach the injection point $point before initiating the +# second change? I'm not sure if there's a small race condition because +# of waking up and detaching the injection point. +sub test_checksum_transition +{ + my ($start, $first, $second, $init, $point, $final) = @_; + + # print the current test instructions, both into TAP output and into + # the server log, to make correlation easier + + my $initstr = 'undef'; + $initstr = $init if defined($init); + + $node->safe_psql('postgres', + "SELECT '========== " . $start . " / " . $first . " / " . $second . " / " . $initstr . " / " . $point . " / " . $final . " =========='"); + + note($start . " / " . $first . " / " . $second . " / " . $initstr . " / " . $point . " / " . $final); + + # put the cluster into the initial checksum state, synchronously + note('changing checksums into initial state: ' . $start); + enable_data_checksums($node, wait => 'on') if ($start eq 'enabled'); + disable_data_checksums($node, wait => 'off') if ($start eq 'disabled'); + + # Wait on an injection point outside a critical section, to initialize + # the shmem (which can't be done in critical section). + attach_injection_point($node, $init) if defined($init); + + # Wait on the injection point, to pause the first checksum change. + note('attaching injection point: ' . $point); + $node->safe_psql('postgres', + "SELECT injection_points_attach('" . $point . "','wait');" + ); + + # Trigger the checksum change, asynchronously + note("triggering first checksum change: " . $first); + enable_data_checksums($node) if ($first eq 'enable'); + disable_data_checksums($node) if ($first eq 'disable'); + + # Handle the initial injection point - wait, wakeup and detatch. This + # initializes the shmem for the 'wait' action. + wait_injection_point($node, $init) if defined($init); + wakeup_injection_point($node, $init) if defined($init); + + # Wait for the actual injection point to be hit by the state change. + wait_injection_point($node, $point); + + # The first checksum state change is waitinig on the injection point. + # Trigger the concurrent change (also asynchronously). + note("triggering second checksum change: " . $second); + enable_data_checksums($node) if ($second eq 'enable'); + disable_data_checksums($node) if ($second eq 'disable'); + + # Wake the injection point, so that the first change can proceed. + wakeup_injection_point($node, $point); + + # Wait until there are no ongoing checksum changes, which we determine + # by looking for a checksum launcher process. + note('wait for the checksum launcher to exit'); + $node->poll_query_until('postgres', + "SELECT count(*) = 0 " + . "FROM pg_catalog.pg_stat_activity " + . "WHERE backend_type = 'datachecksum launcher';"); + + # Does the final checksum state match the expected state? + test_checksum_state($node, $final); + + # Since the log isn't being written to now, parse the log and check + # for instances of checksum verification failures. + my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, + $node_loglocation); + unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in primary log (during WAL recovery)" + ); + $node_loglocation = -s $node->logfile; +} + +# Start the test suite with pgbench running. +background_rw_pgbench($node->port); + +# concurrent enable + disable, different injection points in the "enable" process +test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-inprogress-checksums-start', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-inprogress-checksums-end', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-start', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'off'); +test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-end', 'off'); + +# concurrent enable + enable, different injection points in the "enable" process +test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-start', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'on'); +test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-end', 'on'); + +# concurrent disable + disable, different injection points in the "disable" process +test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'off'); +test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off'); +test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off'); +test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off'); +test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off'); +test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-start', 'off'); +test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off'); +test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'off'); +test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'off'); +test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'off'); +test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'off'); +test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-end', 'off'); + +# concurrent disable + enable, different injection points in the "disable" process +test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-inprogress-checksums-start', 'on'); +test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'on'); +test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on'); +test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'on'); +test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'on'); +test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-start', 'on'); +test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'on'); +test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'on'); +test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'on'); +test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'on'); +test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'on'); +test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-end', 'on'); + +$node->stop; +done_testing(); -- 2.54.0 [text/x-patch] 0006-TAP-11-concurrency-with-checkpoints.patch (12.2K, ../../[email protected]/7-0006-TAP-11-concurrency-with-checkpoints.patch) download | inline diff: From ae1ef6cd0a2c4d2fcddb34db282c55a7f2fe509c Mon Sep 17 00:00:00 2001 From: Tomas Vondra <[email protected]> Date: Wed, 15 Apr 2026 22:22:48 +0200 Subject: [PATCH 06/13] TAP 11: concurrency with checkpoints --- .../t/011_concurrent_checkpoint.pl | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl diff --git a/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl new file mode 100644 index 00000000000..4cea74914d4 --- /dev/null +++ b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl @@ -0,0 +1,276 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster with +# injection point tests injecting failures into the processing + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# This test suite is expensive, or very expensive, to execute. There are two +# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite +# an "checksum_extended" for the full suite. The full suite can run for hours +# on slow or constrained systems. +my $extended = undef; +if ($ENV{PG_TEST_EXTRA}) +{ + $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/); + plan skip_all => 'Expensive data checksums test disabled' + unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/); +} + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +# --------------------------------------------------------------------------- +# Test cluster setup +# + +# Initiate testcluster +my $node = PostgreSQL::Test::Cluster->new('injection_node'); +$node->init(no_data_checksums => 1); +$node->start; + +# Set up test environment +$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +my $pgbench = undef; +my $scalefactor = ($extended ? 10 : 1); +my $node_loglocation = 0; + +$node->command_ok( + [ + 'pgbench', '-p', $node->port, '-i', + '-s', $scalefactor, '-q', 'postgres' + ]); + +# Start a pgbench run in the background against the server specified via the +# port passed as parameter. +sub background_rw_pgbench +{ + my $port = shift; + + # If a previous pgbench is still running, start by shutting it down. + $pgbench->finish if $pgbench; + + my $clients = 1; + my $runtime = 2; + + if ($extended) + { + # Randomize the number of pgbench clients a bit (range 1-16) + $clients = 1 + int(rand(15)); + $runtime = 600; + } + my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients); + + # Randomize whether we spawn connections or not + push(@cmd, '-C') if ($extended && cointoss); + # Finally add the database name to use + push(@cmd, 'postgres'); + + $pgbench = IPC::Run::start( + \@cmd, + '<' => '/dev/null', + '>' => '/dev/null', + '2>' => '/dev/null', + IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default)); +} + +sub attach_injection_point +{ + my ($node, $point) = @_; + note('attaching injection point: ' . $point); + $node->safe_psql('postgres', + "SELECT injection_points_attach('" . $point . "','wait');" + ); +} + +sub wait_injection_point +{ + my ($node, $point) = @_; + + note("waiting for the injection point to be hit"); + $node->poll_query_until( + 'postgres', + "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'", + '1'); +} + +sub wakeup_injection_point +{ + my ($node, $point) = @_; + + note('waking the init injection point: ' . $point); + $node->safe_psql('postgres', + "SELECT injection_points_wakeup('" . $point . "');" + ); + + note("detaching the init injection point"); + $node->safe_psql('postgres', + "SELECT injection_points_detach('" . $point . "');"); +} + +# Test behavior with a checksum transitions and a concurrent checkpoint. +# +# The cluster is initialized into checksum state $start (enabled/disabled), +# and a checksum change is initiated in a controled way. It gets paused at +# a selected injection point $point, and a checkpoint is performed. Then +# the injection point is waken up. Once the checksum change completes, the +# checksum state is compared to the expected $final state. +# +# The checkpoint is performed synchronously, while the checksum worker +# is paused on the injection point. +# +# Finally, after the checksum change completes, the instance is restarted +# (in either fast ot immediate mode, picked randomly), and the final +# checksum state is validated against the expected value. The server log +# is checked for checksum failures. +# +# While the checksum change is happening, there's a r/w pgbench running in +# the background, to generate writes. +# +# arguments: +# +# - start - starting checksum state (enabled/disabled) +# - change - checksum change +# - init - initial injection point (see comment later) +# - point - injection point the first change should wait on +# - final - expected checksum state at the end +# +# XXX Some of the injection points are in a critical section, which does +# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles +# just private memory allocation, but 'wait' requires a shmem allocation. +# To deal with that, we setup an injection point $init outside a critical +# section to initialize the shmem stuff needed by 'wait'. +# +# XXX We could also validate the checksums using pg_checksums, if the +# state is 'on'. +sub test_checksum_transition +{ + my ($start, $change, $init, $point, $final) = @_; + + # print the current test instructions, both into TAP output and into + # the server log, to make correlation easier + + my $initstr = 'undef'; + $initstr = $init if defined($init); + + # Start the test suite with pgbench running. + background_rw_pgbench($node->port); + + $node->safe_psql('postgres', + "SELECT '========== " . $start . " / " . $change . " / " . $initstr . " / " . $point . " / " . $final . " =========='"); + + note($start . " / " . $change . " / " . $initstr . " / " . $point . " / " . $final); + + # put the cluster into the initial checksum state, synchronously + note('changing checksums into initial state: ' . $start); + enable_data_checksums($node, wait => 'on') if ($start eq 'enabled'); + disable_data_checksums($node, wait => 'off') if ($start eq 'disabled'); + + # Wait on an injection point outside a critical section, to initialize + # the shmem (which can't be done in critical section). + attach_injection_point($node, $init) if defined($init); + + # Wait on the injection point, to pause the checksum change. + attach_injection_point($node, $point); + + # Trigger the checksum change, asynchronously + note("triggering checksum change: " . $change); + enable_data_checksums($node) if ($change eq 'enable'); + disable_data_checksums($node) if ($change eq 'disable'); + + # Handle the initial injection point - wait, wakeup and detatch. This + # initializes the shmem for the 'wait' action. + wait_injection_point($node, $init) if defined($init); + wakeup_injection_point($node, $init) if defined($init); + + # Wait for the injection point to be hit by the state change. + wait_injection_point($node, $point); + + # The checksum state change is paused on the injection point. Perform + # the checkpoint (synchronously). + note('checkpoint'); + $node->safe_psql('postgres', "CHECKPOINT"); + + # Wake the injection point, so that the first change can proceed. + wakeup_injection_point($node, $point); + + # Wait until there are no ongoing checksum changes, which we determine + # by looking for a checksum launcher process. + note('wait for the checksum launcher to exit'); + $node->poll_query_until('postgres', + "SELECT count(*) = 0 " + . "FROM pg_catalog.pg_stat_activity " + . "WHERE backend_type = 'datachecksum launcher';"); + + # Does the final checksum state match the expected state? + test_checksum_state($node, $final); + + # restart the cluster, possibly in immediate mode, to simulate a crash + # + # XXX maybe we should do just immediate shutdowns, because the shutdown + # checkpoint will overwrite the state state with "current" one, not the + # state produced by the concurrent change + checkpoint. + $node->stop(stopmode()); + $node->start; + + # Does the final checksum state match the expected state? + test_checksum_state($node, $final); + + # Since the log isn't being written to now, parse the log and check + # for instances of checksum verification failures. + my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, + $node_loglocation); + unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in primary log (during WAL recovery)" + ); + $node_loglocation = -s $node->logfile; +} + +# concurrent enable + checkpoint, different injection points in the "enable" process +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'on'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'on'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'on'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'on'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'on'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'on'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'on'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'on'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-end', 'on'); + +# concurrent disable + checkpoint, different injection points in the "disable" process +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'off'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off'); +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off'); +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'off'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'off'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'off'); +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'off'); +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'off'); +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-end', 'off'); + +$node->stop; +done_testing(); -- 2.54.0 [text/x-patch] 0007-TAP-12-crashes-at-injection-points.patch (13.4K, ../../[email protected]/8-0007-TAP-12-crashes-at-injection-points.patch) download | inline diff: From 57a5e353dbfcaaac1fa7cd25dc38f3e1807ccafa Mon Sep 17 00:00:00 2001 From: Tomas Vondra <[email protected]> Date: Wed, 15 Apr 2026 23:06:14 +0200 Subject: [PATCH 07/13] TAP 12: crashes at injection points --- .../t/012_concurrent_checkpoint_crash.pl | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl diff --git a/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl new file mode 100644 index 00000000000..de169f47034 --- /dev/null +++ b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl @@ -0,0 +1,282 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster with +# injection point tests injecting failures into the processing + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# This test suite is expensive, or very expensive, to execute. There are two +# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite +# an "checksum_extended" for the full suite. The full suite can run for hours +# on slow or constrained systems. +my $extended = undef; +if ($ENV{PG_TEST_EXTRA}) +{ + $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/); + plan skip_all => 'Expensive data checksums test disabled' + unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/); +} + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +# --------------------------------------------------------------------------- +# Test cluster setup +# + +# Initiate testcluster +my $node = PostgreSQL::Test::Cluster->new('injection_node'); +$node->init(no_data_checksums => 1); +$node->start; + +# Set up test environment +$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +my $pgbench = undef; +my $scalefactor = ($extended ? 10 : 1); +my $node_loglocation = 0; + +$node->command_ok( + [ + 'pgbench', '-p', $node->port, '-i', + '-s', $scalefactor, '-q', 'postgres' + ]); + +# Start a pgbench run in the background against the server specified via the +# port passed as parameter. +sub background_rw_pgbench +{ + my $port = shift; + + # If a previous pgbench is still running, start by shutting it down. + $pgbench->finish if $pgbench; + + my $clients = 1; + my $runtime = 2; + + if ($extended) + { + # Randomize the number of pgbench clients a bit (range 1-16) + $clients = 1 + int(rand(15)); + $runtime = 600; + } + my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients); + + # Randomize whether we spawn connections or not + push(@cmd, '-C') if ($extended && cointoss); + # Finally add the database name to use + push(@cmd, 'postgres'); + + $pgbench = IPC::Run::start( + \@cmd, + '<' => '/dev/null', + '>' => '/dev/null', + '2>' => '/dev/null', + IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default)); +} + +sub attach_injection_point +{ + my ($node, $point) = @_; + note('attaching injection point: ' . $point); + $node->safe_psql('postgres', + "SELECT injection_points_attach('" . $point . "','wait');" + ); +} + +sub wait_injection_point +{ + my ($node, $point) = @_; + + note("waiting for the injection point to be hit"); + $node->poll_query_until( + 'postgres', + "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'", + '1'); +} + +sub wakeup_injection_point +{ + my ($node, $point) = @_; + + note('waking the init injection point: ' . $point); + $node->safe_psql('postgres', + "SELECT injection_points_wakeup('" . $point . "');" + ); + + note("detaching the init injection point"); + $node->safe_psql('postgres', + "SELECT injection_points_detach('" . $point . "');"); +} + +# Test behavior with a checksum transitions and a concurrent checkpoint, +# followed by a crash. +# +# The test puts the instance into the initial checksum state $start, +# triggers a checksum change that pauses on a first injection point. Then +# a checkpoint is performed, and the checksum change proceeds either to +# a second injection point or finishes. +# +# Then the instance get restarted in immediate mode to simulate failure, +# and the final checksum state (after recovery) is validated against the +# expected value. The server log is checked for checksum failures. +# +# While the checksum change is happening, there's a r/w pgbench running in +# the background, to generate writes. +# +# arguments: +# +# - start - start checksum state (enabled/disabled) +# - change - checksum change to initiate +# - point1 - injection point before checkpoint +# - point2 - injection point after checkpoint +# - final - expected checksum state at the end +# +# XXX Some of the injection points are in a critical section, which does +# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles +# just private memory allocation, but 'wait' requires a shmem allocation. +# To deal with that, we setup an injection point $init outside a critical +# section to initialize the shmem stuff needed by 'wait'. +# +# XXX We could also validate the checksums using pg_checksums, if the +# state is 'on'. +sub test_checksum_transition +{ + my ($start, $change, $init, $point1, $point2, $final) = @_; + + my $initstr = 'undef'; + my $point2str = 'undef'; + + # print the current test instructions, both into TAP output and into + # the server log, to make correlation easier + + $initstr = $init if defined($init); + $point2str = $point2 if defined($point2); + + $node->safe_psql('postgres', + "SELECT '========== " . $start . " / " . $change . " / " . $initstr . " / " . $point1 . " / " . $point2str . " / " . $final . " =========='"); + + note($start . " / " . $change . " / " . $initstr . " / " . $point1 . " / " . $point2str . " / " . $final); + + # Start the test suite with pgbench running. + background_rw_pgbench($node->port); + + # put the cluster into the initial checksum state, synchronously + note('changing checksums into initial state: ' . $start); + enable_data_checksums($node, wait => 'on') if ($start eq 'enabled'); + disable_data_checksums($node, wait => 'off') if ($start eq 'disabled'); + + # Wait on an injection point outside a critical section, to initialize + # the shmem (which can't be done in critical section). + attach_injection_point($node, $init) if defined($init); + + # Wait on the two injection points, to pause the checksum change. + attach_injection_point($node, $point1); + + # The second injection point is optional, so only attach it then. + attach_injection_point($node, $point2) if defined($point2); + + # Trigger the checksum change, asynchronously + note("triggering checksum change: " . $change); + enable_data_checksums($node) if ($change eq 'enable'); + disable_data_checksums($node) if ($change eq 'disable'); + + # Handle the initial injection point - wait, wakeup and detatch. This + # initializes the shmem for the 'wait' action. + wait_injection_point($node, $init) if defined($init); + wakeup_injection_point($node, $init) if defined($init); + + # Wait for the first injection point to be hit by the state change. + wait_injection_point($node, $point1); + + # The checksum state change is paused on the first injection point. + # Perform the checkpoint (synchronously). + note('checkpoint'); + $node->safe_psql('postgres', "CHECKPOINT"); + + # Wake the injection point, so that the first change can proceed. + wakeup_injection_point($node, $point1); + + # Either wait for the second injection point - if defined, or for the + # checksum change to complete. + + if (defined($point2)) + { + wait_injection_point($node, $point2); + } + else + { + # Wait until there are no ongoing checksum changes, which we determine + # by looking for a checksum launcher process. + note('wait for the checksum launcher to exit'); + $node->poll_query_until('postgres', + "SELECT count(*) = 0 " + . "FROM pg_catalog.pg_stat_activity " + . "WHERE backend_type = 'datachecksum launcher';"); + note('checksum launcher exited'); + } + + # restart the cluster, in immediate mode, to simulate a crash + $node->stop('immediate'); + $node->start; + + # Does the final checksum state match the expected state? + test_checksum_state($node, $final); + + # Since the log isn't being written to now, parse the log and check + # for instances of checksum verification failures. + my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, + $node_loglocation); + unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in primary log (during WAL recovery)" + ); + $node_loglocation = -s $node->logfile; +} + +# concurrent enable + checkpoint, different injection points in the "enable" process +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'off'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'off'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'datachecksums-enable-inprogress-checksums-end', 'off'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'datachecksums-enable-checksums-start', 'off'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'datachecksums-enable-checksums-after-xlogctl', 'off'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'datachecksums-enable-checksums-after-controlfile', 'on'); +test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'datachecksums-enable-checksums-before-checkpoint', 'on'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'datachecksums-enable-checksums-before-barrier-wait', 'on'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'datachecksums-enable-checksums-end', 'on'); +test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-end', undef, 'on'); + +# concurrent disable + checkpoint, different injection points in the "disable" process +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off'); +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'datachecksums-disable-checksums-start', 'off'); +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'datachecksums-disable-checksums-after-xlogctl', 'off'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'datachecksums-disable-checksums-after-controlfile', 'off'); +test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'datachecksums-disable-checksums-before-checkpoint', 'off'); +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'datachecksums-disable-checksums-before-barrier-wait', 'off'); +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'datachecksums-disable-checksums-end', 'off'); +test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-end', undef, 'off'); + +$node->stop; +done_testing(); -- 2.54.0 [text/x-patch] 0008-TAP-13-concurrency-with-checkpoint-REDO.patch (23.8K, ../../[email protected]/9-0008-TAP-13-concurrency-with-checkpoint-REDO.patch) download | inline diff: From ddaca81f530a371a2dfab67ad9da49ef1f8aa69e Mon Sep 17 00:00:00 2001 From: test <test> Date: Fri, 1 May 2026 22:16:00 +0200 Subject: [PATCH 08/13] TAP 13: concurrency with checkpoint REDO --- .../t/013_async_checkpoint_crash.pl | 604 ++++++++++++++++++ 1 file changed, 604 insertions(+) create mode 100644 src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl diff --git a/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl new file mode 100644 index 00000000000..4a310506482 --- /dev/null +++ b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl @@ -0,0 +1,604 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster with +# injection point tests injecting failures into the processing + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# This test suite is expensive, or very expensive, to execute. There are two +# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite +# an "checksum_extended" for the full suite. The full suite can run for hours +# on slow or constrained systems. +my $extended = undef; +if ($ENV{PG_TEST_EXTRA}) +{ + $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/); + plan skip_all => 'Expensive data checksums test disabled' + unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/); +} + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +# --------------------------------------------------------------------------- +# Test cluster setup +# + +# Initiate testcluster +my $node = PostgreSQL::Test::Cluster->new('injection_node'); +$node->init(no_data_checksums => 1); + +# make sure we don't hit checkpoints unless desired +$node->append_conf( + 'postgresql.conf', + qq[ +checkpoint_timeout = 1h +max_wal_size = 32GB +]); +$node->start; + +# Set up test environment +$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +my $pgbench = undef; +my $scalefactor = ($extended ? 10 : 1); +my $node_loglocation = 0; + +$node->command_ok( + [ + 'pgbench', '-p', $node->port, '-i', + '-s', $scalefactor, '-q', 'postgres' + ]); + +# Start a pgbench run in the background against the server specified via the +# port passed as parameter. +sub background_rw_pgbench +{ + my $port = shift; + + # If a previous pgbench is still running, start by shutting it down. + $pgbench->finish if $pgbench; + + my $clients = 1; + my $runtime = 2; + + if ($extended) + { + # Randomize the number of pgbench clients a bit (range 1-16) + $clients = 1 + int(rand(15)); + $runtime = 600; + } + my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients); + + # Randomize whether we spawn connections or not + push(@cmd, '-C') if ($extended && cointoss); + # Finally add the database name to use + push(@cmd, 'postgres'); + + $pgbench = IPC::Run::start( + \@cmd, + '<' => '/dev/null', + '>' => '/dev/null', + '2>' => '/dev/null', + IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default)); +} + +# Start a checkpoint in the background against the server specified via the +# port passed as parameter. +sub background_checkpoint +{ + my $port = shift; + + my @cmd = ('psql', '-p', $port, '-c', 'checkpoint', 'postgres'); + + $pgbench = IPC::Run::start( + \@cmd, + '<' => '/dev/null', + '>' => '/dev/null', + '2>' => '/dev/null', + IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default)); +} + +sub attach_injection_point +{ + my ($node, $point) = @_; + note('attaching injection point: ' . $point); + $node->safe_psql('postgres', + "SELECT injection_points_attach('" . $point . "','wait');" + ); +} + +sub wait_injection_point +{ + my ($node, $point) = @_; + + note("waiting for the injection point to be hit"); + $node->poll_query_until( + 'postgres', + "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'", + '1'); +} + +sub wakeup_injection_point +{ + my ($node, $point) = @_; + + # detach before wakeup, so that we can't hit it right away + note("detaching the init injection point"); + $node->safe_psql('postgres', + "SELECT injection_points_detach('" . $point . "');"); + + note('waking the init injection point: ' . $point); + $node->safe_psql('postgres', + "SELECT injection_points_wakeup('" . $point . "');" + ); +} + +# Test behavior with a checksum transitions and a concurrent checkpoint, +# followed by a crash. +# +# The test puts the instance into the initial checksum state $start, +# initiates a checksum change and waits for it to pause on the $init +# injection point. Then a checkpoint is started (in the background), and +# the two processes go through a list of injection points to interleave +# them in a particular way. +# +# After reaching a $stop injection point, the cluster get restarted in an +# immediate mode to simulate failure. The checksum state (after recovery) +# is validated against the expected value. The server log is checked for +# checksum failures. +# +# While the checksum change is happening, there's a r/w pgbench running in +# the background, to generate writes. +# +# arguments: +# +# - start - start checksum state (enabled/disabled) +# - change - checksum change to initiate +# - init - initial injection point to wait on +# - stop - injection point to wait on before restarting +# - final - expected checksum state at the end +# - steps - a list of injection points to wait on / unpause +# +# XXX Some of the injection points are in a critical section, which does +# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles +# just private memory allocation, but 'wait' requires a shmem allocation. +# To deal with that, we first need to pause on an injection point outside +# a critical section to initialize the shmem stuff needed by 'wait'. +# +# XXX We could also validate the checksums using pg_checksums, if the +# state is 'on'. +# +# XXX The steps are not entirely deterministic, because when unpausing +# an injection point, we don't know if the other process is already +# paused on the following point. +# +sub test_checksum_sequence +{ + my ($start, $change, $init, $stop, $final, @steps) = @_; + + # print the current test instructions, both into TAP output and into + # the server log, to make correlation easier + + $node->safe_psql('postgres', + "SELECT '========== " . $start . " / " . $change . " / (" . @steps . ") / " . $init . " / " . $stop . " / " . $final . " =========='"); + + note($start . " / " . $change . " / (" . @steps . ") / " . $init . " / " . $stop . " / " . $final); + + # Start the test suite with pgbench running. + background_rw_pgbench($node->port); + + # put the cluster into the initial checksum state, synchronously + note('changing checksums into initial state: ' . $start); + enable_data_checksums($node, wait => 'on') if ($start eq 'enabled'); + disable_data_checksums($node, wait => 'off') if ($start eq 'disabled'); + + # attach the initial injection point + attach_injection_point($node, $init); + + # when we're at it, attach the stop injection point + attach_injection_point($node, $stop); + + # Do a checkpoint first, so that we don't accidentaly start the next + # checkpoint at an unecpected time. We want the next checkpoint to be + # the background one. + $node->safe_psql('postgres', "CHECKPOINT"); + + # Trigger the checksum change, asynchronously + note("triggering checksum change: " . $change); + enable_data_checksums($node) if ($change eq 'enable'); + disable_data_checksums($node) if ($change eq 'disable'); + + # wait for the initial injection point + wait_injection_point($node, $init); + + # now attach all the regular injection points + my $n = @steps; + for ($a = 0; $a < $n; $a++) + { + my $point = $steps[$a]; + attach_injection_point($node, $point); + } + + # initiate a background checkpoint + background_checkpoint($node->port); + + # wakeup the initial injection point, to start the main part + wakeup_injection_point($node, $init); + + # wait for regular injection points in sequence, wake and detach them + for ($a = 0; $a < $n; $a++) + { + my $point = $steps[$a]; + + wait_injection_point($node, $point); + wakeup_injection_point($node, $point); + } + + # wait for the stop injection point + wait_injection_point($node, $stop); + + # restart the cluster, in immediate mode, to simulate a crash + $node->stop('immediate'); + $node->start; + + # Does the final checksum state match the expected state? + test_checksum_state($node, $final); + + # Since the log isn't being written to now, parse the log and check + # for instances of checksum verification failures. + my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, + $node_loglocation); + unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in primary log (during WAL recovery)" + ); + $node_loglocation = -s $node->logfile; +} + +my @steps = undef; + +## checksums INPROGRESS ON + +note('TEST INPROGRESS-ON/1'); +@steps = qw(datachecksums-enable-inprogress-checksums-before-xlog + datachecksums-enable-inprogress-checksums-after-xlog + datachecksums-enable-inprogress-checksums-after-xlogctl + datachecksums-enable-inprogress-checksums-after-controlfile + create-checkpoint-initial + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal); +test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps); + +note('TEST INPROGRESS-ON/2'); +@steps = qw(create-checkpoint-initial + datachecksums-enable-inprogress-checksums-before-xlog + datachecksums-enable-inprogress-checksums-after-xlog + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + datachecksums-enable-inprogress-checksums-after-xlogctl + datachecksums-enable-inprogress-checksums-after-controlfile + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal); +test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps); + +note('TEST INPROGRESS-ON/3'); +@steps = qw(create-checkpoint-initial + datachecksums-enable-inprogress-checksums-before-xlog + datachecksums-enable-inprogress-checksums-after-xlog + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + datachecksums-enable-inprogress-checksums-after-xlogctl + datachecksums-enable-inprogress-checksums-after-controlfile + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal); +test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps); + +note('TEST INPROGRESS-ON/4'); +@steps = qw(create-checkpoint-initial + datachecksums-enable-inprogress-checksums-before-xlog + datachecksums-enable-inprogress-checksums-after-xlog + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + datachecksums-enable-inprogress-checksums-after-xlogctl + datachecksums-enable-inprogress-checksums-after-controlfile + checkpoint-before-old-wal-removal); +test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps); + +note('TEST INPROGRESS-ON/5'); +@steps = qw(create-checkpoint-initial + datachecksums-enable-inprogress-checksums-before-xlog + datachecksums-enable-inprogress-checksums-after-xlog + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal + datachecksums-enable-inprogress-checksums-after-xlogctl + datachecksums-enable-inprogress-checksums-after-controlfile); +test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps); + +note('TEST INPROGRESS-ON/6'); +@steps = qw(create-checkpoint-initial + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + datachecksums-enable-inprogress-checksums-before-xlog + datachecksums-enable-inprogress-checksums-after-xlog + datachecksums-enable-inprogress-checksums-after-xlogctl + datachecksums-enable-inprogress-checksums-after-controlfile + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal); +test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps); + +## checksums ON + +note('TEST ON/1'); +@steps = qw(datachecksums-enable-checksums-before-xlog + datachecksums-enable-checksums-after-xlog + datachecksums-enable-checksums-after-xlogctl + datachecksums-enable-checksums-after-controlfile + create-checkpoint-initial + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal); +test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps); + +note('TEST ON/2'); +@steps = qw(create-checkpoint-initial + datachecksums-enable-checksums-before-xlog + datachecksums-enable-checksums-after-xlog + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal + datachecksums-enable-checksums-after-xlogctl + datachecksums-enable-checksums-after-controlfile); +test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps); + +note('TEST ON/3'); +@steps = qw(create-checkpoint-initial + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + datachecksums-enable-checksums-before-xlog + datachecksums-enable-checksums-after-xlog + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal + datachecksums-enable-checksums-after-xlogctl + datachecksums-enable-checksums-after-controlfile); +test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps); + +note('TEST ON/4'); +@steps = qw(create-checkpoint-initial + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + datachecksums-enable-checksums-before-xlog + datachecksums-enable-checksums-after-xlog + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal + datachecksums-enable-checksums-after-xlogctl + datachecksums-enable-checksums-after-controlfile); +test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps); + + +## checksums INPROGRESS OFF + +note('TEST INPROGRESS-OFF/1'); +@steps = qw(datachecksums-disable-inprogress-checksums-before-xlog + datachecksums-disable-inprogress-checksums-after-xlog + datachecksums-disable-inprogress-checksums-after-xlogctl + datachecksums-disable-inprogress-checksums-after-controlfile + datachecksums-disable-inprogress-checksums-before-checkpoint + create-checkpoint-initial + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal + datachecksums-disable-inprogress-checksums-before-barrier-wait); +test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps); + +note('TEST INPROGRESS-OFF/2'); +@steps = qw(create-checkpoint-initial + datachecksums-disable-inprogress-checksums-before-xlog + datachecksums-disable-inprogress-checksums-after-xlog + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal + datachecksums-disable-inprogress-checksums-after-xlogctl + datachecksums-disable-inprogress-checksums-after-controlfile + datachecksums-disable-inprogress-checksums-before-checkpoint + datachecksums-disable-inprogress-checksums-before-barrier-wait); +test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps); + +note('TEST INPROGRESS-OFF/3'); +@steps = qw(create-checkpoint-initial + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + datachecksums-disable-inprogress-checksums-before-xlog + datachecksums-disable-inprogress-checksums-after-xlog + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal + datachecksums-disable-inprogress-checksums-after-xlogctl + datachecksums-disable-inprogress-checksums-after-controlfile + datachecksums-disable-inprogress-checksums-before-checkpoint + datachecksums-disable-inprogress-checksums-before-barrier-wait); +test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps); + +note('TEST INPROGRESS-OFF/4'); +@steps = qw(create-checkpoint-initial + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + datachecksums-disable-inprogress-checksums-before-xlog + datachecksums-disable-inprogress-checksums-after-xlog + datachecksums-disable-inprogress-checksums-after-xlogctl + datachecksums-disable-inprogress-checksums-after-controlfile + datachecksums-disable-inprogress-checksums-before-checkpoint + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal + datachecksums-disable-inprogress-checksums-before-barrier-wait); +test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps); + +## checksums OFF + +note('TEST OFF/1'); +@steps = qw(datachecksums-disable-checksums-before-xlog + datachecksums-disable-checksums-after-xlog + datachecksums-disable-checksums-after-xlogctl + datachecksums-disable-checksums-after-controlfile + datachecksums-disable-checksums-before-checkpoint + create-checkpoint-initial + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal + datachecksums-disable-checksums-before-barrier-wait); +test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); + +note('TEST OFF/2'); +@steps = qw(create-checkpoint-initial + datachecksums-disable-checksums-before-xlog + datachecksums-disable-checksums-after-xlog + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + datachecksums-disable-checksums-after-xlogctl + datachecksums-disable-checksums-after-controlfile + datachecksums-disable-checksums-before-checkpoint + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + checkpoint-before-old-wal-removal + datachecksums-disable-checksums-before-barrier-wait); +test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); + +note('TEST OFF/3'); +@steps = qw(create-checkpoint-initial + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + datachecksums-disable-checksums-before-xlog + datachecksums-disable-checksums-after-xlog + checkpoint-before-redo-wal + checkpoint-after-redo-wal + datachecksums-disable-checksums-after-xlogctl + datachecksums-disable-checksums-after-controlfile + datachecksums-disable-checksums-before-checkpoint + checkpoint-before-old-wal-removal + datachecksums-disable-checksums-before-barrier-wait); +test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); + +note('TEST OFF/4'); +@steps = qw(create-checkpoint-initial + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + datachecksums-disable-checksums-before-xlog + datachecksums-disable-checksums-after-xlog + datachecksums-disable-checksums-after-xlogctl + datachecksums-disable-checksums-after-controlfile + datachecksums-disable-checksums-before-checkpoint + checkpoint-before-old-wal-removal + datachecksums-disable-checksums-before-barrier-wait); +test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); + +note('TEST OFF/5'); +@steps = qw(create-checkpoint-initial + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + checkpoint-before-redo-checksums + datachecksums-disable-checksums-before-xlog + datachecksums-disable-checksums-after-xlog + checkpoint-before-redo-wal + checkpoint-after-redo-wal + datachecksums-disable-checksums-after-xlogctl + datachecksums-disable-checksums-after-controlfile + datachecksums-disable-checksums-before-checkpoint + checkpoint-before-old-wal-removal + datachecksums-disable-checksums-before-barrier-wait); +test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); + +note('TEST OFF/6'); +@steps = qw(create-checkpoint-initial + datachecksums-disable-checksums-before-xlog + datachecksums-disable-checksums-after-xlog + checkpoint-before-redo + checkpoint-before-xlogctl-checksums + checkpoint-after-xlogctl-checksums + datachecksums-disable-checksums-after-xlogctl + checkpoint-before-redo-checksums + checkpoint-before-redo-wal + checkpoint-after-redo-wal + datachecksums-disable-checksums-after-controlfile + datachecksums-disable-checksums-before-checkpoint + checkpoint-before-old-wal-removal + datachecksums-disable-checksums-before-barrier-wait); +test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); + +$node->stop; +done_testing(); -- 2.54.0 [text/x-patch] 0009-TAP-14-checkpoints-with-crashes.patch (12.6K, ../../[email protected]/10-0009-TAP-14-checkpoints-with-crashes.patch) download | inline diff: From ff3cd3e661d508db102e18752fee3335fb4ceaca Mon Sep 17 00:00:00 2001 From: test <test> Date: Fri, 1 May 2026 13:35:56 +0200 Subject: [PATCH 09/13] TAP 14: checkpoints with crashes This shows the non-determinism / race condition with the step in test 012_crashes, thanks to better sequencing of the two sides (checksum worker + checkpointer). The related 012 test is this: test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'datachecksums-enable-checksums-after-xlogctl', 'off'); --- ...ncurrent_checkpoint_crash_deterministic.pl | 359 ++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl diff --git a/src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl b/src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl new file mode 100644 index 00000000000..bfca3983965 --- /dev/null +++ b/src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl @@ -0,0 +1,359 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster with +# injection point tests injecting failures into the processing + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# This test suite is expensive, or very expensive, to execute. There are two +# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite +# an "checksum_extended" for the full suite. The full suite can run for hours +# on slow or constrained systems. +my $extended = undef; +if ($ENV{PG_TEST_EXTRA}) +{ + $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/); + plan skip_all => 'Expensive data checksums test disabled' + unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/); +} + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +# --------------------------------------------------------------------------- +# Test cluster setup +# + +# Initiate testcluster +my $node = PostgreSQL::Test::Cluster->new('injection_node'); +$node->init(no_data_checksums => 1); +$node->start; + +# Set up test environment +$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +my $pgbench = undef; +my $scalefactor = ($extended ? 10 : 1); +my $node_loglocation = 0; + +$node->command_ok( + [ + 'pgbench', '-p', $node->port, '-i', + '-s', $scalefactor, '-q', 'postgres' + ]); + +# Start a pgbench run in the background against the server specified via the +# port passed as parameter. +sub background_rw_pgbench +{ + my $port = shift; + + # If a previous pgbench is still running, start by shutting it down. + $pgbench->finish if $pgbench; + + my $clients = 1; + my $runtime = 2; + + if ($extended) + { + # Randomize the number of pgbench clients a bit (range 1-16) + $clients = 1 + int(rand(15)); + $runtime = 600; + } + my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients); + + # Randomize whether we spawn connections or not + push(@cmd, '-C') if ($extended && cointoss); + # Finally add the database name to use + push(@cmd, 'postgres'); + + $pgbench = IPC::Run::start( + \@cmd, + '<' => '/dev/null', + '>' => '/dev/null', + '2>' => '/dev/null', + IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default)); +} + +# Run a SQL in the background against the server specified via the +# port passed as parameter. +sub background_sql +{ + my ($port, $sql) = @_; + + my @cmd = ('psql', '-p', $port, '-c', $sql, 'postgres'); + + $pgbench = IPC::Run::start( + \@cmd, + '<' => '/dev/null', + '>' => '/dev/null', + '2>' => '/dev/null', + IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default)); +} + +sub attach_injection_point +{ + my ($node, $point) = @_; + note('attaching injection point: ' . $point); + $node->safe_psql('postgres', + "SELECT injection_points_attach('" . $point . "','wait');" + ); +} + +sub wait_injection_point +{ + my ($node, $point) = @_; + + note("waiting for the injection point to be hit"); + $node->poll_query_until( + 'postgres', + "SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'", + '1'); +} + +sub wakeup_injection_point +{ + my ($node, $point) = @_; + + # detach before wakeup, so that we can't hit it right away + note("detaching the init injection point"); + $node->safe_psql('postgres', + "SELECT injection_points_detach('" . $point . "');"); + + note('waking the init injection point: ' . $point); + $node->safe_psql('postgres', + "SELECT injection_points_wakeup('" . $point . "');" + ); +} + +# Test behavior with a checksum transitions and a concurrent checkpoint, +# followed by a crash. +# +# The test puts the instance into the initial checksum state $start, +# triggers a checksum change. A concurrent checkpoint is performed, and +# and the test steps through the checksum change and the checkpoint in +# a deterministic way. +# +# Then the instance get restarted in immediate mode to simulate failure, +# and the final checksum state (after recovery) is validated against the +# expected value. The server log is checked for checksum failures. +# +# While the checksum change is happening, there's a r/w pgbench running in +# the background, to generate writes. +# +# arguments: +# +# - name - name of the test sequence (for easy identification) +# - start - start checksum state (enabled/disabled) +# - change - checksum change to initiate +# - final - expected checksum state at the end +# - steps - steps to go through (wait, wakeup, sql) +# +# XXX This is similar to 012_concurrent_checkpoint_crash, except that the +# checkpoint happens asynchronously (in the background), and the steps +# step through the injection points in a deterministic way. +# +# XXX Some of the injection points are in a critical section, which does +# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles +# just private memory allocation, but 'wait' requires a shmem allocation. +# To deal with that, we setup an injection point $init outside a critical +# section to initialize the shmem stuff needed by 'wait'. This needs to +# be done for individual processes (e.g. the checkpointer needs to init a +# 'wait' point too, it's not enough to init one in the checksum worker). +# +# XXX We could also validate the checksums using pg_checksums, if the +# state is 'on'. +sub test_checksum_sequence +{ + my ($name, $start, $change, $final, @steps) = @_; + + # Start the test suite with pgbench running. + background_rw_pgbench($node->port); + + # print the current test instructions, both into TAP output and into + # the server log, to make correlation easier + + note($name); + $node->safe_psql('postgres', + "SELECT '========== " . $name . " =========='"); + + # put the cluster into the initial checksum state, synchronously + note('changing checksums into initial state: ' . $start); + enable_data_checksums($node, wait => 'on') if ($start eq 'enabled'); + disable_data_checksums($node, wait => 'off') if ($start eq 'disabled'); + + # attach all the injection points mentioned in 'wait' steps + my $n = @steps; + my $s; + note('processing ' . $n . ' steps'); + for ($s = 0; $s < $n; $s++) + { + my @step = $steps[$s]; + my $action = $steps[$s][0]; + my $value = $steps[$s][1]; + + note('step ' . $s . ' action ' . $action . ' / ' . $value); + if ($action eq 'wait') + { + attach_injection_point($node, $value); + } + } + + # Trigger the checksum change, asynchronously + note("triggering checksum change: " . $change); + enable_data_checksums($node) if ($change eq 'enable'); + disable_data_checksums($node) if ($change eq 'disable'); + + # now process all the steps - wait, wakeup, sql, etc. + $n = @steps; + note('processing ' . $n . ' steps'); + for ($s = 0; $s < $n; $s++) + { + my @step = $steps[$s]; + my $action = $steps[$s][0]; + my $value = $steps[$s][1]; + + note('step ' . $s . ' action ' . $action . ' / ' . $value); + if ($action eq 'wait') + { + wait_injection_point($node, $value); + } + elsif ($action eq 'wakeup') + { + wakeup_injection_point($node, $value); + } + elsif ($action eq 'sql') + { + note('sql: ' . $value); + + # initiate a background sql + background_sql($node->port, $value); + } + } + + # restart the cluster, in immediate mode, to simulate a crash + $node->stop('immediate'); + $node->start; + + # Does the final checksum state match the expected state? + test_checksum_state($node, $final); + + # Since the log isn't being written to now, parse the log and check + # for instances of checksum verification failures. + my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, + $node_loglocation); + unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in primary log (during WAL recovery)" + ); + $node_loglocation = -s $node->logfile; +} + +# sequence of steps to perform, each step is defined as an array of actions +# +# - type of an action (wait, wakeup, sql) +# - injection point or SQL command +# +# after processing all commands, the instance gets killed / shut down with +# immediate mode +my @steps = undef; + +# CHECKSUMS-ON-1 +# +# checkpoint after: datachecksums-enable-checksums-after-xlog +# crash after: datachecksums-enable-checksums-after-xlogctl +# checkpoint completes +# +# steps: +# 1) checksums: write XLOG2_CHECKSUMS to WAL +# 2) start checkpoint +# 3) checkpointer: read XLogCtl->data_checksum_version +# 4) checksums: update XLogCtl->data_checksum_version +# 5) checkpointer: write CHECKPOINT_REDO +# 6) checkpointer: complete the checkpoint +# 7) crash +@steps = ( + ['wait', 'datachecksums-enable-checksums-start'], # initialize the wait + ['wakeup', 'datachecksums-enable-checksums-start'], + ['wait', 'datachecksums-enable-checksums-after-xlog'], + ['sql', 'checkpoint'], + ['wait', 'create-checkpoint-initial'], # initialize the wait + ['wakeup', 'create-checkpoint-initial'], + ['wait', 'checkpoint-before-redo-wal'], + ['wakeup', 'datachecksums-enable-checksums-after-xlog'], + ['wait', 'datachecksums-enable-checksums-after-xlogctl'], + ['wakeup', 'checkpoint-before-redo-wal'], + ['wait', 'checkpoint-before-old-wal-removal'], + ['wakeup', 'checkpoint-before-old-wal-removal'] +); + +test_checksum_sequence('CHECKSUMS-ON-1', 'disabled', 'enable', 'off', @steps); + +# CHECKSUMS-ON-2 +# +# checkpoint after: datachecksums-enable-checksums-after-xlog +# crash after: datachecksums-enable-checksums-after-xlogctl +# checkpoint completes +# +# steps: +# 1) checksums: write XLOG2_CHECKSUMS to WAL +# 2) start checkpoint +# 3) checksums: update XLogCtl->data_checksum_version +# 4) checkpointer: read XLogCtl->data_checksum_version +# 5) checkpointer: write CHECKPOINT_REDO +# 6) checkpointer: complete the checkpoint +# 7) crash +@steps = ( + ['wait', 'datachecksums-enable-checksums-start'], # initialize the wait + ['wakeup', 'datachecksums-enable-checksums-start'], + ['wait', 'datachecksums-enable-checksums-after-xlog'], + ['sql', 'checkpoint'], + ['wait', 'create-checkpoint-initial'], # initialize the wait + ['wakeup', 'datachecksums-enable-checksums-after-xlog'], + ['wakeup', 'create-checkpoint-initial'], + ['wait', 'checkpoint-before-xlogctl-checksums'], + ['wait', 'datachecksums-enable-checksums-after-xlogctl'], + ['wakeup', 'checkpoint-before-xlogctl-checksums'], + ['wait', 'checkpoint-before-old-wal-removal'], + ['wakeup', 'checkpoint-before-old-wal-removal'] +); + +test_checksum_sequence('CHECKSUMS-ON-2', 'disabled', 'enable', 'off', @steps); + +# CHECKSUMS-ON-3 +# +# checkpoint happens after checksum worker updates XLogCtl +@steps = ( + ['wait', 'datachecksums-enable-checksums-start'], # initialize the wait + ['wakeup', 'datachecksums-enable-checksums-start'], + ['wait', 'datachecksums-enable-checksums-after-xlogctl'], + ['sql', 'checkpoint'], + ['wait', 'create-checkpoint-initial'], # initialize the wait + ['wakeup', 'create-checkpoint-initial'], + ['wait', 'checkpoint-before-redo-checksums'], + ['wakeup', 'checkpoint-before-redo-checksums'], + ['wait', 'checkpoint-before-old-wal-removal'], + ['wakeup', 'checkpoint-before-old-wal-removal'] +); + +test_checksum_sequence('CHECKSUMS-ON-3', 'disabled', 'enable', 'off', @steps); + +## FIXME do similar sequences for the opposite direction (enabled -> disabled) + +$node->stop; +done_testing(); -- 2.54.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-05 07:43 SATYANARAYANA NARLAPURAM <[email protected]> parent: Tomas Vondra <[email protected]> 2 siblings, 1 reply; 24+ messages in thread From: SATYANARAYANA NARLAPURAM @ 2026-05-05 07:43 UTC (permalink / raw) To: Tomas Vondra <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> Hi Hackers, Daniel, Further testing this feature, I noticed that the cost_delay and cost_limit arguments to pg_enable_data_checksums() in practice have no effect. It appears we have two independent issues in DataChecksumsWorkerMain(): (1) The worker writes the user-supplied values to VacuumCostDelay and VacuumCostLimit (the GUC-bound globals in globals.c). However, vacuum_delay_point() reads vacuum_cost_delay / vacuum_cost_limit declared in vacuum.c. The two pairs are kept in sync only by VacuumUpdateCosts(), which the worker never calls. Therefore, the napping formula always sees the defaults (vacuum_cost_delay = 0) and never sleeps. (2) The worker also resets VacuumCostPageHit/Miss/Dirty to 0 at startup. With all per-page weights at zero, VacuumCostBalance never reaches vacuum_cost_limit, which would defeat the throttling on its own even if (1) were fixed. Repro: Create a database and load data (say 3 GB) SELECT pg_disable_data_checksums(); SELECT pg_enable_data_checksums(100, 1); -- 100 ms/page, balance limit 1 Without the fix, this completes in ~10 seconds and pg_stat_activity never shows wait_event = VacuumDelay for the worker. With even moderate parameters (e.g. (50, 200)) the worker is continuously in VacuumDelay after the patch, and total runtime stretches as one would expect. Also manually tested with cost_delay 0 and higher cost limits. Attached a patch to fix this. Thanks, Satya Attachments: [application/octet-stream] 0001-Apply-data-checksum-worker-throttling-parameters.patch (2.8K, ../../CAHg+QDeevH6aTyWdXYBJW0wOmfoZy66gDi5TfinK_dXeCrHQLg@mail.gmail.com/3-0001-Apply-data-checksum-worker-throttling-parameters.patch) download | inline diff: From 967a25ec5ad9d515a432360dfb9c67d92d8ea491 Mon Sep 17 00:00:00 2001 From: Satya Narlapuram <[email protected]> Date: Tue, 5 May 2026 07:12:10 +0000 Subject: [PATCH] Apply data-checksum worker throttling parameters The DataChecksumsWorker accepts cost_delay and cost_limit parameters from pg_enable_data_checksums() so users can throttle the rewrite I/O caused by enabling checksums online, but the values were silently ignored: * The worker assigned the user-supplied values to VacuumCostDelay and VacuumCostLimit (the GUC-bound globals in globals.c), but vacuum_delay_point() consults vacuum_cost_delay/vacuum_cost_limit declared in vacuum.c. The two pairs are kept in sync by VacuumUpdateCosts(), which the worker never called. As a result the napping formula in vacuum_delay_point() always saw the boot defaults (vacuum_cost_delay = 0), so no throttling occurred. * The worker also reset VacuumCostPageHit/Miss/Dirty to 0 at startup. With all per-page weights at zero, VacuumCostBalance never reaches vacuum_cost_limit, which would defeat the throttling on its own even if the first issue had not existed. Fix both issues by calling VacuumUpdateCosts() after assigning the user values (both at worker startup and on the runtime cost-update path), and by leaving the page-cost weights at their GUC-controlled defaults. --- src/backend/postmaster/datachecksum_state.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index d0d6acdd..c7c05933 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -1538,14 +1538,14 @@ DataChecksumsWorkerMain(Datum arg) * provides rather than inventing something bespoke. This is an internal * implementation detail and care should be taken to avoid it bleeding * through to the user to avoid confusion. + * + * VacuumUpdateCosts() propagates the values to the variables actually + * read by vacuum_delay_point(). */ VacuumCostDelay = DataChecksumState->cost_delay; VacuumCostLimit = DataChecksumState->cost_limit; - VacuumCostActive = (VacuumCostDelay > 0); + VacuumUpdateCosts(); VacuumCostBalance = 0; - VacuumCostPageHit = 0; - VacuumCostPageMiss = 0; - VacuumCostPageDirty = 0; /* * Create and set the vacuum strategy as our buffer strategy. @@ -1602,7 +1602,7 @@ DataChecksumsWorkerMain(Datum arg) costs_updated = true; VacuumCostDelay = DataChecksumState->launch_cost_delay; VacuumCostLimit = DataChecksumState->launch_cost_limit; - VacuumCostActive = (VacuumCostDelay > 0); + VacuumUpdateCosts(); DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay; DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit; -- 2.43.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-05 13:46 Daniel Gustafsson <[email protected]> parent: SATYANARAYANA NARLAPURAM <[email protected]> 0 siblings, 2 replies; 24+ messages in thread From: Daniel Gustafsson @ 2026-05-05 13:46 UTC (permalink / raw) To: SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> > While further testing this feature, I realized that ProcessSingleRelationFork() > unconditionally called log_newpage_buffer() for every page of every relation > during pg_enable_data_checksums(). This included unlogged relations, > which by definition never generate WAL for data changes and are reset to their > init fork on any recovery. I've tested your patch, and also expanded the test you wrote a little with a persistence change. > Further testing this feature, I noticed that the cost_delay and cost_limit arguments > to pg_enable_data_checksums() in practice have no effect. Ugh, the API for updating the costs changed between when this code was written and tested, and when it was committed (and since I was the one committing the new API I really should've caught that). Thanks for the report and fix! While hacking on your patches I realized that the regexes for finding page verification failures in the logs were anchoring at the wrong point, the attached 0003 fixes that. Attached are editorialized versions of the patches, as well as my testfix, that I'm planning to go ahead with. -- Daniel Gustafsson Attachments: [application/octet-stream] 0003-Fix-regex-searching-for-page-verification-failures-i.patch (5.4K, ../../[email protected]/2-0003-Fix-regex-searching-for-page-verification-failures-i.patch) download | inline diff: From 271b7a7d59dba6e6230d03b528c1d3807df8cbb0 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson <[email protected]> Date: Tue, 5 May 2026 15:09:41 +0200 Subject: [PATCH 3/3] Fix regex searching for page verification failures in tests The test for finding page verification failures in the logfiles were missing the /m modifier to make sure it anchors to every newline in the search space buffer, and not just the last one. Spotted while adding a test for the recently reported issue with excessive WAL for unlogged relations. Author: Daniel Gustafsson <[email protected]> Discussion: https://postgr.es/m/CAHg+QDeGrpZbNZdLjd_T4b43xKEEXZN0HGhkFm-1bkBdyzK7AQ@mail.gmail.com --- .../modules/test_checksums/t/006_pgbench_single.pl | 6 +++--- .../modules/test_checksums/t/007_pgbench_standby.pl | 12 ++++++------ src/test/modules/test_checksums/t/008_pitr.pl | 2 +- src/test/modules/test_checksums/t/009_fpi.pl | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl index f5ccc1b5b08..b0ccf05fce9 100644 --- a/src/test/modules/test_checksums/t/006_pgbench_single.pl +++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl @@ -211,7 +211,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++) $node_loglocation); unlike( $log, - qr/page verification failed,.+\d$/, + qr/page verification failed,.+\d$/m, "no checksum validation errors in primary log (during WAL recovery)" ); $node_loglocation = -s $node->logfile; @@ -249,7 +249,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++) $node_loglocation); unlike( $log, - qr/page verification failed,.+\d$/, + qr/page verification failed,.+\d$/m, "no checksum validation errors in primary log (outside WAL recovery)" ); $node_loglocation = -s $node->logfile; @@ -276,7 +276,7 @@ my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation); unlike( $log, - qr/page verification failed,.+\d$/, + qr/page verification failed,.+\d$/m, "no checksum validation errors in primary log"); $node_loglocation = -s $node->logfile; diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl index 0b3996f1d69..38e17ca38ee 100644 --- a/src/test/modules/test_checksums/t/007_pgbench_standby.pl +++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl @@ -270,7 +270,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++) $node_primary_loglocation); unlike( $log, - qr/page verification failed,.+\d$/, + qr/page verification failed,.+\d$/m, "no checksum validation errors in primary log (during WAL recovery)" ); $node_primary_loglocation = -s $node_primary->logfile; @@ -299,7 +299,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++) $node_standby_loglocation); unlike( $log, - qr/page verification failed,.+\d$/, + qr/page verification failed,.+\d$/m, "no checksum validation errors in standby_1 log (during WAL recovery)" ); $node_standby_loglocation = -s $node_standby->logfile; @@ -341,7 +341,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++) $node_primary_loglocation); unlike( $log, - qr/page verification failed,.+\d$/, + qr/page verification failed,.+\d$/m, "no checksum validation errors in primary log (outside WAL recovery)" ); $node_primary_loglocation = -s $node_primary->logfile; @@ -363,7 +363,7 @@ for (my $i = 0; $i < $TEST_ITERATIONS; $i++) $node_standby_loglocation); unlike( $log, - qr/page verification failed,.+\d$/, + qr/page verification failed,.+\d$/m, "no checksum validation errors in standby_1 log (outside WAL recovery)" ); $node_standby_loglocation = -s $node_standby->logfile; @@ -394,14 +394,14 @@ my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile, $node_primary_loglocation); unlike( $log, - qr/page verification failed,.+\d$/, + qr/page verification failed,.+\d$/m, "no checksum validation errors in primary log"); $node_primary_loglocation = -s $node_primary->logfile; $log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile, $node_standby_loglocation); unlike( $log, - qr/page verification failed,.+\d$/, + qr/page verification failed,.+\d$/m, "no checksum validation errors in standby_1 log"); $node_standby_loglocation = -s $node_standby->logfile; diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl index 1f8176686fd..913377c9d32 100644 --- a/src/test/modules/test_checksums/t/008_pitr.pl +++ b/src/test/modules/test_checksums/t/008_pitr.pl @@ -186,7 +186,7 @@ $node_pitr->stop; my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0); unlike( $log, - qr/page verification failed,.+\d$/, + qr/page verification failed,.+\d$/m, "no checksum validation errors in pitr log"); done_testing(); diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl index a1cea91f787..1a7dcd0e15d 100644 --- a/src/test/modules/test_checksums/t/009_fpi.pl +++ b/src/test/modules/test_checksums/t/009_fpi.pl @@ -58,7 +58,7 @@ $node->stop; my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0); unlike( $log, - qr/page verification failed,.+\d$/, + qr/page verification failed,.+\d$/m, "no checksum validation errors in server log"); done_testing(); -- 2.39.3 (Apple Git-146) [application/octet-stream] 0002-Apply-data-checksum-worker-throttling-parameters.patch (2.5K, ../../[email protected]/3-0002-Apply-data-checksum-worker-throttling-parameters.patch) download | inline diff: From e62cc373ce6b7f93018b7dd541b89fae0dafb332 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson <[email protected]> Date: Tue, 5 May 2026 15:00:24 +0200 Subject: [PATCH 2/3] Apply data-checksum worker throttling parameters The DataChecksumsWorker accepts cost_delay and cost_limit parameters from pg_enable_data_checksums() so users can throttle the I/O caused by enabling checksums. Due to the API for setting the cost parameters changing between when the code was written, and when it was committed the new cost update function call was omitted and thus the parameters were silently ignored. Fix by calling VacuumUpdateCosts() after assigning the parameters (both during worker startup and on the runtime cost-update path), and by leaving the page-cost weights at their GUC-controlled defaults. Author: SATYANARAYANA NARLAPURAM <[email protected]> Reviewed-by: Daniel Gustafsson <[email protected]> Discussion: https://postgr.es/m/CAHg+QDeevH6aTyWdXYBJW0wOmfoZy66gDi5TfinK_dXeCrHQLg@mail.gmail.com --- src/backend/postmaster/datachecksum_state.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index 535da8b0c4a..7cdc662c7a6 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -1543,14 +1543,14 @@ DataChecksumsWorkerMain(Datum arg) * provides rather than inventing something bespoke. This is an internal * implementation detail and care should be taken to avoid it bleeding * through to the user to avoid confusion. + * + * VacuumUpdateCosts() propagates the values to the variables actually + * read by vacuum_delay_point(). */ VacuumCostDelay = DataChecksumState->cost_delay; VacuumCostLimit = DataChecksumState->cost_limit; - VacuumCostActive = (VacuumCostDelay > 0); + VacuumUpdateCosts(); VacuumCostBalance = 0; - VacuumCostPageHit = 0; - VacuumCostPageMiss = 0; - VacuumCostPageDirty = 0; /* * Create and set the vacuum strategy as our buffer strategy. @@ -1607,7 +1607,7 @@ DataChecksumsWorkerMain(Datum arg) costs_updated = true; VacuumCostDelay = DataChecksumState->launch_cost_delay; VacuumCostLimit = DataChecksumState->launch_cost_limit; - VacuumCostActive = (VacuumCostDelay > 0); + VacuumUpdateCosts(); DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay; DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit; -- 2.39.3 (Apple Git-146) [application/octet-stream] 0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch (5.8K, ../../[email protected]/4-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch) download | inline diff: From 7f5addd7b93132dbb4d8a9a0e45a961df4470805 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson <[email protected]> Date: Tue, 5 May 2026 14:54:23 +0200 Subject: [PATCH 1/3] Skip WAL for unlogged relations during online checksum enable ProcessSingleRelationFork() unconditionally generated an FPI WAL record for every page of every relation when enabling checksums. Unlogged relations, which by definition never generate WAL for data changes, were not exempt which generated excessive WAL to be emitted. Fix by guarding the FPI WAL record call with RelationNeedsWAL() to avoid emitting WAL for unlogged relations. Unlogged pages are still dirtied to ensure the checksum is written to disk at the next checkpoint. A test which creates an unlogged table, enables checksums, and verifies via pg_relation_size() on the standby has been added. Author: SATYANARAYANA NARLAPURAM <[email protected]> Reviewed-by: Daniel Gustafsson <[email protected]> Discussion: https://postgr.es/m/CAHg+QDeGrpZbNZdLjd_T4b43xKEEXZN0HGhkFm-1bkBdyzK7AQ@mail.gmail.com --- src/backend/postmaster/datachecksum_state.c | 9 ++- .../test_checksums/t/003_standby_restarts.pl | 72 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index d0d6acdd6a2..535da8b0c4a 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -690,11 +690,16 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg * at one point in the past, so only when checksums are first on, then * off, and then turned on again. TODO: investigate if this could be * avoided if the checksum is calculated to be correct and wal_level - * is set to "minimal", + * is set to "minimal". + * + * Unlogged relations don't need WAL since they are reset to their + * init fork on recovery. We still dirty the buffer so that the + * checksum is written to disk at the next checkpoint. */ START_CRIT_SECTION(); MarkBufferDirty(buf); - log_newpage_buffer(buf, false); + if (RelationNeedsWAL(reln)) + log_newpage_buffer(buf, false); END_CRIT_SECTION(); UnlockReleaseBuffer(buf); diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl index 11e15c9d734..b79a06b9b32 100644 --- a/src/test/modules/test_checksums/t/003_standby_restarts.pl +++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl @@ -115,6 +115,78 @@ $result = $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1"); is($result, "19998", 'ensure we can safely read all data without checksums'); +# --------------------------------------------------------------------------- +# Test that enabling checksums does not emit WAL for unlogged relations. +# Unlogged relations are wiped on recovery, so FPIs for them would be +# pointless and waste WAL traffic / standby I/O. +# + +$node_primary->safe_psql('postgres', + 'CREATE UNLOGGED TABLE unlogged_tbl AS SELECT generate_series(1,1000) AS a;' +); +$node_primary->wait_for_catchup($node_standby, 'replay', + $node_primary->lsn('insert')); + +# Get the relfilenode and database OID so we can inspect the filesystem +my $unlogged_rfn = $node_primary->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE relname = 'unlogged_tbl';"); +my $db_oid = $node_primary->safe_psql('postgres', + "SELECT oid FROM pg_database WHERE datname = 'postgres';"); + +# Verify the standby only has the init fork (no main fork) +my $standby_datadir = $node_standby->data_dir; +ok( !-f "$standby_datadir/base/$db_oid/$unlogged_rfn", + 'standby has no main fork for unlogged table before enable'); + +# Re-enable data checksums +enable_data_checksums($node_primary, wait => 'on'); +wait_for_checksum_state($node_standby, 'on'); + +# After standby replays, the unlogged main file must still not exist. +# If the bug were present, FPI replay would materialize the full table. +$node_primary->wait_for_catchup($node_standby, 'replay', + $node_primary->lsn('insert')); +ok( !-f "$standby_datadir/base/$db_oid/$unlogged_rfn", + 'standby has no main fork for unlogged table after enable'); + +# Verify unlogged relation size is 0 on the standby (main fork missing) +my $standby_size = $node_standby->safe_psql('postgres', + "SELECT pg_relation_size('unlogged_tbl', 'main');"); +is($standby_size, '0', + 'unlogged table has zero size on standby after checksum enable'); + +# Unlogged table should still be readable on primary +$result = + $node_primary->safe_psql('postgres', 'SELECT count(*) FROM unlogged_tbl;'); +is($result, '1000', + 'unlogged table readable on primary after checksum enable'); + +# Alter persistence to logged, and make sure we can read it on both the primary +# and standby without any page verification errors in the logfiles. +$node_primary->safe_psql('postgres', 'ALTER TABLE unlogged_tbl SET logged;'); +$node_primary->wait_for_catchup($node_standby, 'replay', + $node_primary->lsn('insert')); + +$result = + $node_primary->safe_psql('postgres', 'SELECT sum(a) FROM unlogged_tbl;'); +is($result, '500500', 'previously unlogged table can be read on primary'); +$result = + $node_standby->safe_psql('postgres', 'SELECT sum(a) FROM unlogged_tbl;'); +is($result, '500500', 'previously unlogged table can be read on standby'); + $node_standby->stop; $node_primary->stop; + +# Perform one final pass over the logs and hunt for unexpected errors +my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile, 0); +unlike( + $log, + qr/page verification failed,.+\d$/m, + "no checksum validation errors in primary log"); +$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile, 0); +unlike( + $log, + qr/page verification failed,.+\d$/m, + "no checksum validation errors in standby log"); + done_testing(); -- 2.39.3 (Apple Git-146) ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-05 14:24 SATYANARAYANA NARLAPURAM <[email protected]> parent: Daniel Gustafsson <[email protected]> 1 sibling, 0 replies; 24+ messages in thread From: SATYANARAYANA NARLAPURAM @ 2026-05-05 14:24 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> Hi, On Tue, May 5, 2026 at 6:46 AM Daniel Gustafsson <[email protected]> wrote: > > While further testing this feature, I realized that > ProcessSingleRelationFork() > > unconditionally called log_newpage_buffer() for every page of every > relation > > during pg_enable_data_checksums(). This included unlogged relations, > > which by definition never generate WAL for data changes and are reset to > their > > init fork on any recovery. > > > I've tested your patch, and also expanded the test you wrote a little with > a > persistence change. > > > Further testing this feature, I noticed that the cost_delay and > cost_limit arguments > > to pg_enable_data_checksums() in practice have no effect. > > Ugh, the API for updating the costs changed between when this code was > written > and tested, and when it was committed (and since I was the one committing > the > new API I really should've caught that). Thanks for the report and fix! > Any thoughts on adding an injection point test to verify the values are configured correctly? This can be a different patch, perhaps included in Tomas' tests? > > While hacking on your patches I realized that the regexes for finding page > verification failures in the logs were anchoring at the wrong point, the > attached 0003 fixes that. > > Attached are editorialized versions of the patches, as well as my testfix, > that > I'm planning to go ahead with. > LGTM. Thanks! Thanks, Satya ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-05 15:21 Ayush Tiwari <[email protected]> parent: Daniel Gustafsson <[email protected]> 1 sibling, 1 reply; 24+ messages in thread From: Ayush Tiwari @ 2026-05-05 15:21 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> Hi, On Tue, 5 May 2026 at 19:16, Daniel Gustafsson <[email protected]> wrote: > > While further testing this feature, I realized that > ProcessSingleRelationFork() > > unconditionally called log_newpage_buffer() for every page of every > relation > > during pg_enable_data_checksums(). This included unlogged relations, > > which by definition never generate WAL for data changes and are reset to > their > > init fork on any recovery. > > > I've tested your patch, and also expanded the test you wrote a little with > a > persistence change. > > I tested the patches, it mostly looks good. I've a small concern in 0001. The new guard uses only RelationNeedsWAL(reln), but ProcessSingleRelationByOid() iterates all forks. For unlogged relations, the init fork is special, there are several existing call sites that preserve WAL for INIT_FORKNUM, for example using RelationNeedsWAL(rel) || forknum == INIT_FORKNUM and catalog/storage.c notes that unlogged init forks need WAL and sync. So I think the condition in ProcessSingleRelationFork() should preserve the init-fork case, e.g. if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM) log_newpage_buffer(buf, false); It would also be good to extend the new test so it exercises a non-empty init fork, perhaps with an unlogged table that has an index, and then verifies the standby/recovery behavior. 0002 and 0003 look good to me. Regards, Ayush ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-05 19:08 Daniel Gustafsson <[email protected]> parent: Ayush Tiwari <[email protected]> 0 siblings, 2 replies; 24+ messages in thread From: Daniel Gustafsson @ 2026-05-05 19:08 UTC (permalink / raw) To: Ayush Tiwari <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> > On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]> wrote: > I've a small concern in 0001. The new guard uses only RelationNeedsWAL(reln), > but ProcessSingleRelationByOid() iterates all forks. For unlogged relations, > the init fork is special, there are several existing call sites that preserve > WAL for INIT_FORKNUM, for example using > > RelationNeedsWAL(rel) || forknum == INIT_FORKNUM > > and catalog/storage.c notes that unlogged init forks need WAL and sync. > > So I think the condition in ProcessSingleRelationFork() should preserve the > init-fork case, e.g. > > if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM) > log_newpage_buffer(buf, false); Which failure scenario are you thinking about here? When dealing with the catalog relation I can see the need but here we are reading, and writing, data pages. In which case would we need to issue an FPI for an unlogged relation init fork? I might be missing something obvious here. > 0002 and 0003 look good to me. Thanks for looking! -- Daniel Gustafsson ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-05 19:18 Ayush Tiwari <[email protected]> parent: Daniel Gustafsson <[email protected]> 1 sibling, 1 reply; 24+ messages in thread From: Ayush Tiwari @ 2026-05-05 19:18 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> Hi, On Wed, 6 May 2026 at 00:38, Daniel Gustafsson <[email protected]> wrote: > > On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]> > wrote: > > > I've a small concern in 0001. The new guard uses only > RelationNeedsWAL(reln), > > but ProcessSingleRelationByOid() iterates all forks. For unlogged > relations, > > the init fork is special, there are several existing call sites that > preserve > > WAL for INIT_FORKNUM, for example using > > > > RelationNeedsWAL(rel) || forknum == INIT_FORKNUM > > > > and catalog/storage.c notes that unlogged init forks need WAL and sync. > > > > So I think the condition in ProcessSingleRelationFork() should preserve > the > > init-fork case, e.g. > > > > if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM) > > log_newpage_buffer(buf, false); > > Which failure scenario are you thinking about here? When dealing with the > catalog relation I can see the need but here we are reading, and writing, > data > pages. In which case would we need to issue an FPI for an unlogged > relation > init fork? I might be missing something obvious here. > The case I was thinking about is not the unlogged relation contents themselves, but the init fork used as the reset template. Some unlogged indexes can have initialized pages in the init fork, and recovery later copies that fork to the main fork when resetting unlogged relations. So my concern was that, during online checksum enable, we might update the checksum state of an init-fork page on the primary but not WAL-log an FPI for it because RelationNeedsWAL(reln) is false. Then a standby, or recovery after a crash, could still have the old version of that init fork. If that fork is later copied to the main fork after checksums are enabled, it might lead to checksum verification failures? Maybe there is another guarantee that makes this impossible, but I did not see it from the patch/test. That is why I wondered whether the condition should preserve the existing special treatment for INIT_FORKNUM. Regards, Ayush ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-05 19:19 SATYANARAYANA NARLAPURAM <[email protected]> parent: Daniel Gustafsson <[email protected]> 1 sibling, 0 replies; 24+ messages in thread From: SATYANARAYANA NARLAPURAM @ 2026-05-05 19:19 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> Hi On Tue, May 5, 2026 at 12:08 PM Daniel Gustafsson <[email protected]> wrote: > > On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]> > wrote: > > > I've a small concern in 0001. The new guard uses only > RelationNeedsWAL(reln), > > but ProcessSingleRelationByOid() iterates all forks. For unlogged > relations, > > the init fork is special, there are several existing call sites that > preserve > > WAL for INIT_FORKNUM, for example using > > > > RelationNeedsWAL(rel) || forknum == INIT_FORKNUM > > > > and catalog/storage.c notes that unlogged init forks need WAL and sync. > > > > So I think the condition in ProcessSingleRelationFork() should preserve > the > > init-fork case, e.g. > > > > if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM) > > log_newpage_buffer(buf, false); > > Which failure scenario are you thinking about here? When dealing with the > catalog relation I can see the need but here we are reading, and writing, > data > pages. In which case would we need to issue an FPI for an unlogged > relation > init fork? I might be missing something obvious here. Good catch,IIUC init page has valid checksum and when we set checksum on primary I think we need to pass it to standby. Otherwise, after failover we may see invalid checksum for unlogged relations. I haven’t validated it, will test it and update the patch. ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-05 19:45 SATYANARAYANA NARLAPURAM <[email protected]> parent: Ayush Tiwari <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: SATYANARAYANA NARLAPURAM @ 2026-05-05 19:45 UTC (permalink / raw) To: Ayush Tiwari <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> Hi, On Tue, May 5, 2026 at 12:19 PM Ayush Tiwari <[email protected]> wrote: > Hi, > > On Wed, 6 May 2026 at 00:38, Daniel Gustafsson <[email protected]> wrote: > >> > On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]> >> wrote: >> >> > I've a small concern in 0001. The new guard uses only >> RelationNeedsWAL(reln), >> > but ProcessSingleRelationByOid() iterates all forks. For unlogged >> relations, >> > the init fork is special, there are several existing call sites that >> preserve >> > WAL for INIT_FORKNUM, for example using >> > >> > RelationNeedsWAL(rel) || forknum == INIT_FORKNUM >> > >> > and catalog/storage.c notes that unlogged init forks need WAL and sync. >> > >> > So I think the condition in ProcessSingleRelationFork() should preserve >> the >> > init-fork case, e.g. >> > >> > if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM) >> > log_newpage_buffer(buf, false); >> >> Which failure scenario are you thinking about here? When dealing with the >> catalog relation I can see the need but here we are reading, and writing, >> data >> pages. In which case would we need to issue an FPI for an unlogged >> relation >> init fork? I might be missing something obvious here. >> > > The case I was thinking about is not the unlogged relation contents > themselves, but the init fork used as the reset template. Some unlogged > indexes can have initialized pages in the init fork, and recovery later > copies > that fork to the main fork when resetting unlogged relations. > > So my concern was that, during online checksum enable, we might update the > checksum state of an init-fork page on the primary but not WAL-log an FPI > for > it because RelationNeedsWAL(reln) is false. Then a standby, or recovery > after > a crash, could still have the old version of that init fork. If that fork > is > later copied to the main fork after checksums are enabled, it might lead to > checksum verification failures? > > Maybe there is another guarantee that makes this impossible, but I did not > see > it from the patch/test. That is why I wondered whether the condition > should > preserve the existing special treatment for INIT_FORKNUM. > It is a bug in the code, I added a test in the v2 patch to test this scenario and the test failed earlier. Thanks, Satya > Attachments: [application/octet-stream] v2-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch (6.3K, ../../CAHg+QDfR0NHqWmE50R42Wi=Bz7Gw9Q-p84TA=vragN_nfjmnew@mail.gmail.com/3-v2-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch) download | inline diff: From 7f5addd7b93132dbb4d8a9a0e45a961df4470806 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson <[email protected]> Date: Tue, 5 May 2026 14:54:23 +0200 Subject: [PATCH v3 1/3] Skip WAL for unlogged relations during online checksum enable ProcessSingleRelationFork() unconditionally generated an FPI WAL record for every page of every relation when enabling checksums. Unlogged relations, which by definition never generate WAL for data changes, were not exempt which generated excessive WAL to be emitted. Fix by guarding the FPI WAL record call with RelationNeedsWAL() to avoid emitting WAL for unlogged main forks. Unlogged pages are still dirtied to ensure the checksum is written to disk at the next checkpoint. The init fork remains WAL-logged even for unlogged relations, because that is what the standby uses to materialize the relation after promotion (see ResetUnloggedRelations()). Skipping init-fork WAL would leave the standby with a stale init fork that, once copied to the main fork on promotion, would fail checksum verification on every read of the unlogged relation. A test which creates an unlogged table with an index, enables checksums, promotes the standby, and verifies that the unlogged relation and its indexes are still readable post-promotion has been added. Author: SATYANARAYANA NARLAPURAM <[email protected]> Reviewed-by: Daniel Gustafsson <[email protected]> Discussion: https://postgr.es/m/CAHg+QDeGrpZbNZdLjd_T4b43xKEEXZN0HGhkFm-1bkBdyzK7AQ@mail.gmail.com --- src/backend/postmaster/datachecksum_state.c | 16 +++- .../test_checksums/t/003_standby_restarts.pl | 65 ++++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index c7c0593345..fb29662423 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -690,11 +690,23 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg * at one point in the past, so only when checksums are first on, then * off, and then turned on again. TODO: investigate if this could be * avoided if the checksum is calculated to be correct and wal_level - * is set to "minimal", + * is set to "minimal". + * + * Unlogged relations don't need WAL since they are reset to their + * init fork on recovery. We still dirty the buffer so that the + * checksum is written to disk at the next checkpoint. + * + * The init fork is an exception: it is WAL-logged so the standby + * can materialize the relation after promotion (see + * ResetUnloggedRelations()). Skipping it here would leave the + * standby with a stale init fork that, once copied to the main + * fork on promotion, would fail checksum verification on every + * read. */ START_CRIT_SECTION(); MarkBufferDirty(buf); - log_newpage_buffer(buf, false); + if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM) + log_newpage_buffer(buf, false); END_CRIT_SECTION(); UnlockReleaseBuffer(buf); diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl index 11e15c9d73..26b68b98e6 100644 --- a/src/test/modules/test_checksums/t/003_standby_restarts.pl +++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl @@ -115,6 +115,69 @@ $result = $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1"); is($result, "19998", 'ensure we can safely read all data without checksums'); -$node_standby->stop; +# --------------------------------------------------------------------------- +# Test that enabling checksums on a cluster with unlogged relations does not +# break those relations after a standby promotion. Unlogged relations only +# WAL-log their init fork; that init fork is what ResetUnloggedRelations() +# copies to the main fork on promotion. If checksum-enable on the primary +# does not WAL-log init-fork rewrites, the standby keeps a stale init fork +# and the post-promotion main fork fails verification on every read. +# + +# Use a btree index so the init fork is non-trivial (one metapage) +$node_primary->safe_psql( + 'postgres', q[ + CREATE UNLOGGED TABLE unlogged_tbl (id int PRIMARY KEY, + payload text); + INSERT INTO unlogged_tbl + SELECT g, repeat('x', 100) FROM generate_series(1, 1000) g; + CREATE INDEX unlogged_tbl_payload_idx ON unlogged_tbl (payload); +]); +$node_primary->wait_for_catchup($node_standby, 'replay', + $node_primary->lsn('insert')); + +# Re-enable data checksums and wait for both nodes to converge to "on" +enable_data_checksums($node_primary, wait => 'on'); +wait_for_checksum_state($node_standby, 'on'); +$node_primary->wait_for_catchup($node_standby, 'replay', + $node_primary->lsn('insert')); + +# Promote the standby and verify the unlogged relation is still usable. +# Without the init-fork WAL fix, every read of the index would fail with +# "page verification failed, calculated checksum X but expected 0". $node_primary->stop; +$node_standby->promote; + +$result = + $node_standby->safe_psql('postgres', 'SELECT count(*) FROM unlogged_tbl;'); +is($result, '0', + 'unlogged table readable on promoted standby (truncated as expected)'); + +$node_standby->safe_psql('postgres', + "INSERT INTO unlogged_tbl SELECT g, repeat('y',100) FROM generate_series(1,100) g;" +); +$result = $node_standby->safe_psql('postgres', + 'SET enable_seqscan = off; SELECT id FROM unlogged_tbl WHERE id = 50;'); +is($result, '50', 'indexed lookup on promoted standby returns expected row'); + +$node_standby->safe_psql('postgres', 'CREATE EXTENSION IF NOT EXISTS amcheck;'); +$node_standby->safe_psql('postgres', + "SELECT bt_index_check('unlogged_tbl_pkey'::regclass);"); +$node_standby->safe_psql('postgres', + "SELECT bt_index_check('unlogged_tbl_payload_idx'::regclass);"); + +$node_standby->stop; + +# Final sanity sweep of the logs for any checksum failures +my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile, 0); +unlike( + $log, + qr/page verification failed,.+\d$/m, + "no checksum validation errors in primary log"); +$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile, 0); +unlike( + $log, + qr/page verification failed,.+\d$/m, + "no checksum validation errors in standby log"); + done_testing(); -- 2.39.3 (Apple Git-146) ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-05 20:05 SATYANARAYANA NARLAPURAM <[email protected]> parent: SATYANARAYANA NARLAPURAM <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: SATYANARAYANA NARLAPURAM @ 2026-05-05 20:05 UTC (permalink / raw) To: Ayush Tiwari <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> Hi, On Tue, May 5, 2026 at 12:45 PM SATYANARAYANA NARLAPURAM < [email protected]> wrote: > Hi, > > On Tue, May 5, 2026 at 12:19 PM Ayush Tiwari <[email protected]> > wrote: > >> Hi, >> >> On Wed, 6 May 2026 at 00:38, Daniel Gustafsson <[email protected]> wrote: >> >>> > On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]> >>> wrote: >>> >>> > I've a small concern in 0001. The new guard uses only >>> RelationNeedsWAL(reln), >>> > but ProcessSingleRelationByOid() iterates all forks. For unlogged >>> relations, >>> > the init fork is special, there are several existing call sites that >>> preserve >>> > WAL for INIT_FORKNUM, for example using >>> > >>> > RelationNeedsWAL(rel) || forknum == INIT_FORKNUM >>> > >>> > and catalog/storage.c notes that unlogged init forks need WAL and sync. >>> > >>> > So I think the condition in ProcessSingleRelationFork() should >>> preserve the >>> > init-fork case, e.g. >>> > >>> > if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM) >>> > log_newpage_buffer(buf, false); >>> >>> Which failure scenario are you thinking about here? When dealing with >>> the >>> catalog relation I can see the need but here we are reading, and >>> writing, data >>> pages. In which case would we need to issue an FPI for an unlogged >>> relation >>> init fork? I might be missing something obvious here. >>> >> >> The case I was thinking about is not the unlogged relation contents >> themselves, but the init fork used as the reset template. Some unlogged >> indexes can have initialized pages in the init fork, and recovery later >> copies >> that fork to the main fork when resetting unlogged relations. >> >> So my concern was that, during online checksum enable, we might update the >> checksum state of an init-fork page on the primary but not WAL-log an FPI >> for >> it because RelationNeedsWAL(reln) is false. Then a standby, or recovery >> after >> a crash, could still have the old version of that init fork. If that >> fork is >> later copied to the main fork after checksums are enabled, it might lead >> to >> checksum verification failures? >> >> Maybe there is another guarantee that makes this impossible, but I did >> not see >> it from the patch/test. That is why I wondered whether the condition >> should >> preserve the existing special treatment for INIT_FORKNUM. >> > > It is a bug in the code, I added a test in the v2 patch to test this > scenario and the test > failed earlier. > Please find the latest v3. Earlier I took dependency on amcheck and removed it in v3 and instead added queries that forces necessary checks. Thanks, Satya Attachments: [application/octet-stream] v3-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch (8.3K, ../../CAHg+QDfEpF0S7S6rzgjKrvq6HrE_CdKg2KM9mS7dKZQhqKuVtA@mail.gmail.com/3-v3-0001-Skip-WAL-for-unlogged-relations-during-online-checks.patch) download | inline diff: From 7f5addd7b93132dbb4d8a9a0e45a961df4470806 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson <[email protected]> Date: Tue, 5 May 2026 14:54:23 +0200 Subject: [PATCH v3 1/3] Skip WAL for unlogged relations during online checksum enable ProcessSingleRelationFork() unconditionally generated an FPI WAL record for every page of every relation when enabling checksums. Unlogged relations, which by definition never generate WAL for data changes, were not exempt which generated excessive WAL to be emitted. Fix by guarding the FPI WAL record call with RelationNeedsWAL() to avoid emitting WAL for unlogged main forks. Unlogged pages are still dirtied to ensure the checksum is written to disk at the next checkpoint. The init fork remains WAL-logged even for unlogged relations, because that is what the standby uses to materialize the relation after promotion (see ResetUnloggedRelations()). Skipping init-fork WAL would leave the standby with a stale init fork that, once copied to the main fork on promotion, would fail checksum verification on every read of the unlogged relation. A test which creates an unlogged table with an index, enables checksums, promotes the standby, and verifies that the unlogged relation and its indexes are still readable post-promotion has been added. Author: SATYANARAYANA NARLAPURAM <[email protected]> Reviewed-by: Daniel Gustafsson <[email protected]> Discussion: https://postgr.es/m/CAHg+QDeGrpZbNZdLjd_T4b43xKEEXZN0HGhkFm-1bkBdyzK7AQ@mail.gmail.com --- src/backend/postmaster/datachecksum_state.c | 16 ++- .../test_checksums/t/003_standby_restarts.pl | 111 ++++++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index c7c0593345..fb29662423 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -690,11 +690,23 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg * at one point in the past, so only when checksums are first on, then * off, and then turned on again. TODO: investigate if this could be * avoided if the checksum is calculated to be correct and wal_level - * is set to "minimal", + * is set to "minimal". + * + * Unlogged relations don't need WAL since they are reset to their + * init fork on recovery. We still dirty the buffer so that the + * checksum is written to disk at the next checkpoint. + * + * The init fork is an exception: it is WAL-logged so the standby + * can materialize the relation after promotion (see + * ResetUnloggedRelations()). Skipping it here would leave the + * standby with a stale init fork that, once copied to the main + * fork on promotion, would fail checksum verification on every + * read. */ START_CRIT_SECTION(); MarkBufferDirty(buf); - log_newpage_buffer(buf, false); + if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM) + log_newpage_buffer(buf, false); END_CRIT_SECTION(); UnlockReleaseBuffer(buf); diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl index 11e15c9d73..5bbf38ed21 100644 --- a/src/test/modules/test_checksums/t/003_standby_restarts.pl +++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl @@ -115,6 +115,115 @@ $result = $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1"); is($result, "19998", 'ensure we can safely read all data without checksums'); -$node_standby->stop; +# --------------------------------------------------------------------------- +# Test that enabling checksums does not emit WAL for unlogged relations. +# Unlogged relations are wiped on recovery, so FPIs for them would be +# pointless and waste WAL traffic / standby I/O. +# +# Additionally, exercise standby promotion to ensure the init fork of an +# unlogged relation is still WAL-logged during checksum enable -- otherwise +# the standby keeps a stale init fork and the post-promotion main fork +# fails verification on every read (see ResetUnloggedRelations()). Both +# tables must exist BEFORE enable_data_checksums() so that their init +# forks get re-checksummed during the enable sweep. +# + +$node_primary->safe_psql('postgres', + 'CREATE UNLOGGED TABLE unlogged_tbl AS SELECT generate_series(1,1000) AS a;' +); +# Use a btree index so the init fork is non-trivial (one metapage). +$node_primary->safe_psql( + 'postgres', q[ + CREATE UNLOGGED TABLE unlogged_promo (id int PRIMARY KEY, + payload text); + INSERT INTO unlogged_promo + SELECT g, repeat('x', 100) FROM generate_series(1, 1000) g; + CREATE INDEX unlogged_promo_payload_idx ON unlogged_promo (payload); +]); +$node_primary->wait_for_catchup($node_standby, 'replay', + $node_primary->lsn('insert')); + +# Get the relfilenode and database OID so we can inspect the filesystem +my $unlogged_rfn = $node_primary->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE relname = 'unlogged_tbl';"); +my $db_oid = $node_primary->safe_psql('postgres', + "SELECT oid FROM pg_database WHERE datname = 'postgres';"); + +# Verify the standby only has the init fork (no main fork) +my $standby_datadir = $node_standby->data_dir; +ok( !-f "$standby_datadir/base/$db_oid/$unlogged_rfn", + 'standby has no main fork for unlogged table before enable'); + +# Re-enable data checksums +enable_data_checksums($node_primary, wait => 'on'); +wait_for_checksum_state($node_standby, 'on'); + +# After standby replays, the unlogged main file must still not exist. +# If the bug were present, FPI replay would materialize the full table. +$node_primary->wait_for_catchup($node_standby, 'replay', + $node_primary->lsn('insert')); +ok( !-f "$standby_datadir/base/$db_oid/$unlogged_rfn", + 'standby has no main fork for unlogged table after enable'); + +# Verify unlogged relation size is 0 on the standby (main fork missing) +my $standby_size = $node_standby->safe_psql('postgres', + "SELECT pg_relation_size('unlogged_tbl', 'main');"); +is($standby_size, '0', + 'unlogged table has zero size on standby after checksum enable'); + +# Unlogged table should still be readable on primary +$result = + $node_primary->safe_psql('postgres', 'SELECT count(*) FROM unlogged_tbl;'); +is($result, '1000', + 'unlogged table readable on primary after checksum enable'); + +# Alter persistence to logged, and make sure we can read it on both the primary +# and standby without any page verification errors in the logfiles. +$node_primary->safe_psql('postgres', 'ALTER TABLE unlogged_tbl SET logged;'); +$node_primary->wait_for_catchup($node_standby, 'replay', + $node_primary->lsn('insert')); + +$result = + $node_primary->safe_psql('postgres', 'SELECT sum(a) FROM unlogged_tbl;'); +is($result, '500500', 'previously unlogged table can be read on primary'); +$result = + $node_standby->safe_psql('postgres', 'SELECT sum(a) FROM unlogged_tbl;'); +is($result, '500500', 'previously unlogged table can be read on standby'); + +# --------------------------------------------------------------------------- +# Promote the standby and verify the unlogged_promo relation (created above +# before the enable sweep) is still usable. Without the init-fork WAL fix, +# every read of the index would fail with +# "page verification failed, calculated checksum X but expected 0". +# $node_primary->stop; +$node_standby->promote; + +$result = + $node_standby->safe_psql('postgres', + 'SELECT count(*) FROM unlogged_promo;'); +is($result, '0', + 'unlogged table readable on promoted standby (truncated as expected)'); + +$node_standby->safe_psql('postgres', + "INSERT INTO unlogged_promo SELECT g, repeat('y',100) FROM generate_series(1,100) g;" +); +$result = $node_standby->safe_psql('postgres', + 'SET enable_seqscan = off; SELECT id FROM unlogged_promo WHERE id = 50;'); +is($result, '50', 'indexed lookup on promoted standby returns expected row'); + +$node_standby->stop; + +# Perform one final pass over the logs and hunt for unexpected errors +my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile, 0); +unlike( + $log, + qr/page verification failed,.+\d$/m, + "no checksum validation errors in primary log"); +$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile, 0); +unlike( + $log, + qr/page verification failed,.+\d$/m, + "no checksum validation errors in standby log"); + done_testing(); -- 2.39.3 (Apple Git-146) ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-05 21:01 Ayush Tiwari <[email protected]> parent: SATYANARAYANA NARLAPURAM <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Ayush Tiwari @ 2026-05-05 21:01 UTC (permalink / raw) To: SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> Hi, On Wed, 6 May 2026 at 01:36, SATYANARAYANA NARLAPURAM < [email protected]> wrote: > Hi, > > On Tue, May 5, 2026 at 12:45 PM SATYANARAYANA NARLAPURAM < > [email protected]> wrote: > >> Hi, >> >> On Tue, May 5, 2026 at 12:19 PM Ayush Tiwari <[email protected]> >> wrote: >> >>> Hi, >>> >>> On Wed, 6 May 2026 at 00:38, Daniel Gustafsson <[email protected]> wrote: >>> >>>> > On 5 May 2026, at 17:21, Ayush Tiwari <[email protected]> >>>> wrote: >>>> >>>> > I've a small concern in 0001. The new guard uses only >>>> RelationNeedsWAL(reln), >>>> > but ProcessSingleRelationByOid() iterates all forks. For unlogged >>>> relations, >>>> > the init fork is special, there are several existing call sites that >>>> preserve >>>> > WAL for INIT_FORKNUM, for example using >>>> > >>>> > RelationNeedsWAL(rel) || forknum == INIT_FORKNUM >>>> > >>>> > and catalog/storage.c notes that unlogged init forks need WAL and >>>> sync. >>>> > >>>> > So I think the condition in ProcessSingleRelationFork() should >>>> preserve the >>>> > init-fork case, e.g. >>>> > >>>> > if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM) >>>> > log_newpage_buffer(buf, false); >>>> >>>> Which failure scenario are you thinking about here? When dealing with >>>> the >>>> catalog relation I can see the need but here we are reading, and >>>> writing, data >>>> pages. In which case would we need to issue an FPI for an unlogged >>>> relation >>>> init fork? I might be missing something obvious here. >>>> >>> >>> The case I was thinking about is not the unlogged relation contents >>> themselves, but the init fork used as the reset template. Some unlogged >>> indexes can have initialized pages in the init fork, and recovery later >>> copies >>> that fork to the main fork when resetting unlogged relations. >>> >>> So my concern was that, during online checksum enable, we might update >>> the >>> checksum state of an init-fork page on the primary but not WAL-log an >>> FPI for >>> it because RelationNeedsWAL(reln) is false. Then a standby, or recovery >>> after >>> a crash, could still have the old version of that init fork. If that >>> fork is >>> later copied to the main fork after checksums are enabled, it might lead >>> to >>> checksum verification failures? >>> >>> Maybe there is another guarantee that makes this impossible, but I did >>> not see >>> it from the patch/test. That is why I wondered whether the condition >>> should >>> preserve the existing special treatment for INIT_FORKNUM. >>> >> >> It is a bug in the code, I added a test in the v2 patch to test this >> scenario and the test >> failed earlier. >> > > Please find the latest v3. Earlier I took dependency on amcheck and > removed it in v3 > and instead added queries that forces necessary checks. > Thanks, the v3 patch addresses my concern. I also checked the new test by temporarily removing the INIT_FORKNUM exception, and it failed on the promoted standby while reading the unlogged relation, so the test does cover the init fork failure scenario. The code change looks right to me now. Regards, Ayush ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-06 15:22 Daniel Gustafsson <[email protected]> parent: Ayush Tiwari <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Daniel Gustafsson @ 2026-05-06 15:22 UTC (permalink / raw) To: Ayush Tiwari <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Tomas Vondra <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> > On 5 May 2026, at 23:01, Ayush Tiwari <[email protected]> wrote: > Thanks, the v3 patch addresses my concern. Thanks both of you for the report, patch and testing. Now I see what you meant yesterday and I agree with the conclusion. I've pushed 0001-0003 today with minor tweaks. -- Daniel Gustafsson ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-26 18:12 Tomas Vondra <[email protected]> parent: Tomas Vondra <[email protected]> 2 siblings, 1 reply; 24+ messages in thread From: Tomas Vondra @ 2026-05-26 18:12 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> Hi, On 5/4/26 15:16, Tomas Vondra wrote: > Hi, > > ... snip ... > > Let me briefly explain what the various TAP tests aim to do. From the > very beginning, my main concern regarding this patch was race conditions > when updating the shared state about effective data_checksum_version. > Because the state is effectively split into about three or four places: > > * LocalDataChecksumVersion (local cache) > * XLogCtl->data_checksum_version (XLogCtl->info_lck) > * ControlFile->data_checksum_version (ControlFileLock) > * state in control file on disk > > These pieces are protected by different locks, the protocol for updating > and/or reading the various flags is not trivial (and some of the fixed > issues were due to ControlFile->data_checksum_version being updated from > a place that shouldn't have). > I still have a funny feeling about the "global checksum version" stored in multiple places with separate locks, and the protocol when updating them (e.g. which process does that, when, etc.). I haven't found any fundamental correctness issue, though. I kept looking for issues, and I realized StartupXLOG may not do the right thing on the standby after promotion. if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON) { XLogChecksums(PG_DATA_CHECKSUM_OFF); SpinLockAcquire(&XLogCtl->info_lck); XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF; SetLocalDataChecksumState(XLogCtl->data_checksum_version); SpinLockRelease(&XLogCtl->info_lck); ereport(...); } Consider this sequence of steps: 1) primary/standby cluster has checksums OFF 2) primary starts enabling checksums 3) primary moves to inprogress-on 4) standby receives that and moves to inprogress-on too 5) primary crashes / shuts down / ... 6) standby gets promoted, and does the StartupXLOG thing 7) standby moves from inprogress-on back to off However, as there's no EmitProcSignalBarrier(), existing backends on the hot standby won't be notified about the "off" change. And so there will be a somewhat inconsistent checksum state, with new backends knowing it's "off", and old backends thinking it's still inprogress-on. It's not difficult to reproduce this. I guess the existing TAP tests may not catch this because they always open a new connection when checking the state, and so see the new (and correct) version. I believe a similar issue happens for the inprogress-off case a couple lines later, but I haven't tried reproducing that one. Ultimately, neither of these cases should cause checksum failures, because the "correct" new state is "off", and the old backends are not verifying checksums either. I suppose this means we should not be updating the checksum state without emitting the barrier? I think all other places do that. > > ... snip ... > > The TAP 012 tests checksum change with a concurrent checkpoint, followed > by a crash, and tests the final state. It pauses the change at an > injection point, does a checkpoint, proceeds to the next injection > point, crashes and does recovery. The expectation is that the final > state "flips" at some injection point, once it gets further enough, and > stays there. But what actually happens is this: > > a) test_checksum_transition( > 'disabled', 'enable', undef, > 'datachecksums-enable-inprogress-checksums-end', > 'datachecksums-enable-checksums-start', > 'off'); > > b) test_checksum_transition( > 'disabled', 'enable', undef, > 'datachecksums-enable-checksums-start', > 'datachecksums-enable-checksums-after-xlog', > 'on'); > > c) test_checksum_transition( > 'disabled', 'enable', 'datachecksums-enable-checksums-start', > 'datachecksums-enable-checksums-after-xlogs', > 'datachecksums-enable-checksums-after-xlogctl', > 'off'); > > This says that if the checkpoint happens after > 'datachecksums-enable-inprogress-checksums-end' or after > 'datachecksums-enable-checksums-after-xlog', we end up with 'off' (i.e. > enabling checksums fails). > > But if the checkpoint happens after > 'datachecksums-enable-checksums-start', we end up with "on" (after > recovery). > > This is a bit surprising, because that injection point is before > 'datachecksums-enable-checksums-after-xlog'. So the enabling process > gets further and further, but the final state flips off -> on -> off, > contradicting the expectation that it changes once. > > I haven't quite wrapped my head around it yet, but my understanding is > this is due to a race condition between the checksum launcher (writing > XLOG2_CHECKSUMS and updating the shmem state), and the checkpointer > (reading the shmem state and generating REDO). > > The launcher does this sequence of steps: > > 1) write XLOG2_CHECKSUMS with new state > 2) update XLogCtl->data_checksum_version > 3) update ControlFile->data_checksum_version > 4) UpdateControlFile() > 5) emits barrier > > while the checkpointer (CreateCheckPoint) does this: > > A) read XLogCtl->data_checksum_version (while holding insert locks) > B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version) > C) UpdateControlFile() > > The outcome depends on how exactly these two sequences interleave. For > example, this can happen: > > 1) write XLOG2_CHECKSUMS with new state > A) read XLogCtl->data_checksum_version (while holding insert locks) > B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version) > C) UpdateControlFile() > 2) update XLogCtl->data_checksum_version > 3) update ControlFile->data_checksum_version > 4) UpdateControlFile() > 5) emits barrier > > Which means the XLOG_CHECKPOINT_REDO will be after XLOG2_CHECKSUMS (and > so redo won't see it), but the checkpoint will still get the old > checksum state from XLogCtl. And so the outcome is "off", per case (c). > > But it can also happen what case (b) does: > > A) read XLogCtl->data_checksum_version (while holding insert locks) > B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version) > C) UpdateControlFile() > 1) write XLOG2_CHECKSUMS with new state > 2) update XLogCtl->data_checksum_version > 3) update ControlFile->data_checksum_version > 4) UpdateControlFile() > 5) emits barrier > > In which case the REDO will have the old state, but the recovery will > read the XLOG2_CHECKSUMS, and so end up with "on". > > This is the root cause of the surprising behavior in TAP 012, I think. > > I attempted to trigger these race conditions in TAP 013, but without > much success. In the end I realized it probably needs more control, > waiting for the other process to hit the next injection point before > unpausing the current one. TAP 014 does that, and it shows that with the > right interleaving of steps the (c) case can end up with both "on" and > "off" final state. > > As I said, I don't claim I fully understand this yet. But I wouldn't > call this "bug" - AFAICS it won't produce an incorrect final state (I > haven't seen any such cases). > > Still, I wonder if there's a potential issue I failed to notice. > I kept thinking about this non-determinism, and I still think I'm right about the root cause. The checkpointer and datachecksum worker may interleave in different ways, affecting the final checksum state. The existing interlock (or lack of of it) is not sufficient. To verify this hypothesis, I've done a simple thing - I've introduced a new LWLock, protecting the "critical part" so that the checkpoint_redo can't happen in between XLogChecksums() and XLogCtl update. Patch attached, but I don't propose this for commit, I'm not sure if it's right (or safe), it's merely a PoC to confirm the issue. But it resolves the weird cases, simply by prohibiting some of the step sequences (so some of the tests needed to be commented out, as they'd block). I'm still not sure if it really is an issue or just an annoyance, because I've not been able to find a case where it'd lead to checksum failures (or obviously incorrect final state after recovery). > > The other question I had when looking at this (concurrency with > checkpoints) is what we get by doing > > MyProc->delayChkptFlags |= DELAY_CHKPT_START; > > whenever updating the state in SetDataChecksums... functions. Because > the only thing that guarantees is the updates happen on one side of the > checkpoint record. What does that give us, actually? > > It does not seem to prevent this surprising behavior, and it does not > say the XLOG2_CHECKSUMS happens before/after the XLOG_CHECKPOINT_REDO. > I still don't understand why this needs DELAY_CHKPT_START ... I also noticed a couple minor comment issues, per attached patch (this may need pgindent). regards -- Tomas Vondra Attachments: [text/x-patch] checksums-extra-lwlock.patch (24.9K, ../../[email protected]/2-checksums-extra-lwlock.patch) download | inline diff: diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 483e3ea77f5..cb1a77aabb8 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -4772,6 +4772,8 @@ SetDataChecksumsOnInProgress(void) INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-before-xlog", NULL); + LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE); + XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON); INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlog", NULL); @@ -4781,6 +4783,8 @@ SetDataChecksumsOnInProgress(void) XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON; SpinLockRelease(&XLogCtl->info_lck); + LWLockRelease(ChecksumStateLock); + elog(LOG, "SetDataChecksumsOnInProgress XLogCtl->data_checksum_version %u => %u", data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON); @@ -4875,6 +4879,8 @@ SetDataChecksumsOn(void) INJECTION_POINT_CACHED("datachecksums-enable-checksums-before-xlog", NULL); + LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE); + XLogChecksums(PG_DATA_CHECKSUM_VERSION); INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlog", NULL); @@ -4884,6 +4890,8 @@ SetDataChecksumsOn(void) XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION; SpinLockRelease(&XLogCtl->info_lck); + LWLockRelease(ChecksumStateLock); + elog(LOG, "SetDataChecksumsOn / XLogCtl->data_checksum_version %u => %u", data_checksum_version, PG_DATA_CHECKSUM_VERSION); @@ -4980,6 +4988,8 @@ SetDataChecksumsOff(void) INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-before-xlog", NULL); + LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE); + XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF); INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlog", NULL); @@ -4989,6 +4999,8 @@ SetDataChecksumsOff(void) XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF; SpinLockRelease(&XLogCtl->info_lck); + LWLockRelease(ChecksumStateLock); + elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u", data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF); @@ -5050,6 +5062,8 @@ SetDataChecksumsOff(void) INJECTION_POINT_CACHED("datachecksums-disable-checksums-before-xlog", NULL); + LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE); + XLogChecksums(PG_DATA_CHECKSUM_OFF); INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlog", NULL); @@ -5059,6 +5073,8 @@ SetDataChecksumsOff(void) XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF; SpinLockRelease(&XLogCtl->info_lck); + LWLockRelease(ChecksumStateLock); + elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u", data_checksum_version, PG_DATA_CHECKSUM_OFF); @@ -7771,6 +7787,9 @@ CreateCheckPoint(int flags) INJECTION_POINT_CACHED("checkpoint-before-redo-checksums", NULL); + /* make XLogChecksums + XLogCtl update atomic */ + LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE); + WALInsertLockAcquire(); redo_rec.wal_level = wal_level; SpinLockAcquire(&XLogCtl->info_lck); @@ -7785,6 +7804,8 @@ CreateCheckPoint(int flags) XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo)); (void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO); + LWLockRelease(ChecksumStateLock); + /* * XLogInsertRecord will have updated XLogCtl->Insert.RedoRecPtr in * shared memory and RedoRecPtr in backend-local memory, but we need diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 560659f9568..a9b1e213c7e 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -370,6 +370,7 @@ WaitLSN "Waiting to read or update shared Wait-for-LSN state." LogicalDecodingControl "Waiting to read or update logical decoding status information." DataChecksumsWorker "Waiting for data checksums worker." AioWorkerControl "Waiting to update AIO worker information." +ChecksumState "Waiting to update data checksum state." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index d7eb648bd27..2e566c4cbe3 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -89,6 +89,7 @@ PG_LWLOCK(54, WaitLSN) PG_LWLOCK(55, LogicalDecodingControl) PG_LWLOCK(56, DataChecksumsWorker) PG_LWLOCK(57, AioWorkerControl) +PG_LWLOCK(58, ChecksumState) /* * There also exist several built-in LWLock tranches. As with the predefined diff --git a/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl index 4cea74914d4..c6f7842caa9 100644 --- a/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl +++ b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl @@ -245,13 +245,13 @@ sub test_checksum_transition # concurrent enable + checkpoint, different injection points in the "enable" process test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'on'); -test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on'); +#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on'); test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on'); test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on'); test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'on'); test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'on'); test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'on'); -test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on'); +#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on'); test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'on'); test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'on'); test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'on'); @@ -260,12 +260,12 @@ test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-chec # concurrent disable + checkpoint, different injection points in the "disable" process test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'off'); -test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off'); +#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off'); test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off'); test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off'); test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off'); test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'off'); -test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off'); +#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off'); test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'off'); test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'off'); test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'off'); diff --git a/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl index de169f47034..77c34b8b16d 100644 --- a/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl +++ b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl @@ -251,13 +251,13 @@ sub test_checksum_transition # concurrent enable + checkpoint, different injection points in the "enable" process test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'off'); -test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off'); +#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off'); test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off'); test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'off'); test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'datachecksums-enable-inprogress-checksums-end', 'off'); test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'datachecksums-enable-checksums-start', 'off'); test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on'); -test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'datachecksums-enable-checksums-after-xlogctl', 'off'); +#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'datachecksums-enable-checksums-after-xlogctl', 'off'); test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'datachecksums-enable-checksums-after-controlfile', 'on'); test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'datachecksums-enable-checksums-before-checkpoint', 'on'); test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'datachecksums-enable-checksums-before-barrier-wait', 'on'); @@ -266,12 +266,12 @@ test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-chec # concurrent disable + checkpoint, different injection points in the "disable" process test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off'); -test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on'); +#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on'); test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off'); test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off'); test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'datachecksums-disable-checksums-start', 'off'); test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off'); -test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'datachecksums-disable-checksums-after-xlogctl', 'off'); +#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'datachecksums-disable-checksums-after-xlogctl', 'off'); test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'datachecksums-disable-checksums-after-controlfile', 'off'); test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'datachecksums-disable-checksums-before-checkpoint', 'off'); test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'datachecksums-disable-checksums-before-barrier-wait', 'off'); diff --git a/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl index 4a310506482..0c59f6d4095 100644 --- a/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl +++ b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl @@ -349,20 +349,20 @@ note('TEST INPROGRESS-ON/5'); datachecksums-enable-inprogress-checksums-after-controlfile); test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps); -note('TEST INPROGRESS-ON/6'); -@steps = qw(create-checkpoint-initial - checkpoint-before-redo - checkpoint-before-xlogctl-checksums - checkpoint-after-xlogctl-checksums - checkpoint-before-redo-checksums - datachecksums-enable-inprogress-checksums-before-xlog - datachecksums-enable-inprogress-checksums-after-xlog - datachecksums-enable-inprogress-checksums-after-xlogctl - datachecksums-enable-inprogress-checksums-after-controlfile - checkpoint-before-redo-wal - checkpoint-after-redo-wal - checkpoint-before-old-wal-removal); -test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps); +#note('TEST INPROGRESS-ON/6'); +#@steps = qw(create-checkpoint-initial +# checkpoint-before-redo +# checkpoint-before-xlogctl-checksums +# checkpoint-after-xlogctl-checksums +# checkpoint-before-redo-checksums +# datachecksums-enable-inprogress-checksums-before-xlog +# datachecksums-enable-inprogress-checksums-after-xlog +# datachecksums-enable-inprogress-checksums-after-xlogctl +# datachecksums-enable-inprogress-checksums-after-controlfile +# checkpoint-before-redo-wal +# checkpoint-after-redo-wal +# checkpoint-before-old-wal-removal); +#test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps); ## checksums ON @@ -395,20 +395,20 @@ note('TEST ON/2'); datachecksums-enable-checksums-after-controlfile); test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps); -note('TEST ON/3'); -@steps = qw(create-checkpoint-initial - checkpoint-before-redo - checkpoint-before-xlogctl-checksums - checkpoint-after-xlogctl-checksums - checkpoint-before-redo-checksums - datachecksums-enable-checksums-before-xlog - datachecksums-enable-checksums-after-xlog - checkpoint-before-redo-wal - checkpoint-after-redo-wal - checkpoint-before-old-wal-removal - datachecksums-enable-checksums-after-xlogctl - datachecksums-enable-checksums-after-controlfile); -test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps); +#note('TEST ON/3'); +#@steps = qw(create-checkpoint-initial +# checkpoint-before-redo +# checkpoint-before-xlogctl-checksums +# checkpoint-after-xlogctl-checksums +# checkpoint-before-redo-checksums +# datachecksums-enable-checksums-before-xlog +# datachecksums-enable-checksums-after-xlog +# checkpoint-before-redo-wal +# checkpoint-after-redo-wal +# checkpoint-before-old-wal-removal +# datachecksums-enable-checksums-after-xlogctl +# datachecksums-enable-checksums-after-controlfile); +#test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps); note('TEST ON/4'); @steps = qw(create-checkpoint-initial @@ -462,39 +462,39 @@ note('TEST INPROGRESS-OFF/2'); datachecksums-disable-inprogress-checksums-before-barrier-wait); test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps); -note('TEST INPROGRESS-OFF/3'); -@steps = qw(create-checkpoint-initial - checkpoint-before-redo - checkpoint-before-xlogctl-checksums - checkpoint-after-xlogctl-checksums - checkpoint-before-redo-checksums - datachecksums-disable-inprogress-checksums-before-xlog - datachecksums-disable-inprogress-checksums-after-xlog - checkpoint-before-redo-wal - checkpoint-after-redo-wal - checkpoint-before-old-wal-removal - datachecksums-disable-inprogress-checksums-after-xlogctl - datachecksums-disable-inprogress-checksums-after-controlfile - datachecksums-disable-inprogress-checksums-before-checkpoint - datachecksums-disable-inprogress-checksums-before-barrier-wait); -test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps); - -note('TEST INPROGRESS-OFF/4'); -@steps = qw(create-checkpoint-initial - checkpoint-before-redo - checkpoint-before-xlogctl-checksums - checkpoint-after-xlogctl-checksums - checkpoint-before-redo-checksums - datachecksums-disable-inprogress-checksums-before-xlog - datachecksums-disable-inprogress-checksums-after-xlog - datachecksums-disable-inprogress-checksums-after-xlogctl - datachecksums-disable-inprogress-checksums-after-controlfile - datachecksums-disable-inprogress-checksums-before-checkpoint - checkpoint-before-redo-wal - checkpoint-after-redo-wal - checkpoint-before-old-wal-removal - datachecksums-disable-inprogress-checksums-before-barrier-wait); -test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps); +#note('TEST INPROGRESS-OFF/3'); +#@steps = qw(create-checkpoint-initial +# checkpoint-before-redo +# checkpoint-before-xlogctl-checksums +# checkpoint-after-xlogctl-checksums +# checkpoint-before-redo-checksums +# datachecksums-disable-inprogress-checksums-before-xlog +# datachecksums-disable-inprogress-checksums-after-xlog +# checkpoint-before-redo-wal +# checkpoint-after-redo-wal +# checkpoint-before-old-wal-removal +# datachecksums-disable-inprogress-checksums-after-xlogctl +# datachecksums-disable-inprogress-checksums-after-controlfile +# datachecksums-disable-inprogress-checksums-before-checkpoint +# datachecksums-disable-inprogress-checksums-before-barrier-wait); +#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps); + +#note('TEST INPROGRESS-OFF/4'); +#@steps = qw(create-checkpoint-initial +# checkpoint-before-redo +# checkpoint-before-xlogctl-checksums +# checkpoint-after-xlogctl-checksums +# checkpoint-before-redo-checksums +# datachecksums-disable-inprogress-checksums-before-xlog +# datachecksums-disable-inprogress-checksums-after-xlog +# datachecksums-disable-inprogress-checksums-after-xlogctl +# datachecksums-disable-inprogress-checksums-after-controlfile +# datachecksums-disable-inprogress-checksums-before-checkpoint +# checkpoint-before-redo-wal +# checkpoint-after-redo-wal +# checkpoint-before-old-wal-removal +# datachecksums-disable-inprogress-checksums-before-barrier-wait); +#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps); ## checksums OFF @@ -532,22 +532,22 @@ note('TEST OFF/2'); datachecksums-disable-checksums-before-barrier-wait); test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); -note('TEST OFF/3'); -@steps = qw(create-checkpoint-initial - checkpoint-before-redo - checkpoint-before-xlogctl-checksums - checkpoint-after-xlogctl-checksums - checkpoint-before-redo-checksums - datachecksums-disable-checksums-before-xlog - datachecksums-disable-checksums-after-xlog - checkpoint-before-redo-wal - checkpoint-after-redo-wal - datachecksums-disable-checksums-after-xlogctl - datachecksums-disable-checksums-after-controlfile - datachecksums-disable-checksums-before-checkpoint - checkpoint-before-old-wal-removal - datachecksums-disable-checksums-before-barrier-wait); -test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); +#note('TEST OFF/3'); +#@steps = qw(create-checkpoint-initial +# checkpoint-before-redo +# checkpoint-before-xlogctl-checksums +# checkpoint-after-xlogctl-checksums +# checkpoint-before-redo-checksums +# datachecksums-disable-checksums-before-xlog +# datachecksums-disable-checksums-after-xlog +# checkpoint-before-redo-wal +# checkpoint-after-redo-wal +# datachecksums-disable-checksums-after-xlogctl +# datachecksums-disable-checksums-after-controlfile +# datachecksums-disable-checksums-before-checkpoint +# checkpoint-before-old-wal-removal +# datachecksums-disable-checksums-before-barrier-wait); +#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); note('TEST OFF/4'); @steps = qw(create-checkpoint-initial @@ -566,22 +566,22 @@ note('TEST OFF/4'); datachecksums-disable-checksums-before-barrier-wait); test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); -note('TEST OFF/5'); -@steps = qw(create-checkpoint-initial - checkpoint-before-redo - checkpoint-before-xlogctl-checksums - checkpoint-after-xlogctl-checksums - checkpoint-before-redo-checksums - datachecksums-disable-checksums-before-xlog - datachecksums-disable-checksums-after-xlog - checkpoint-before-redo-wal - checkpoint-after-redo-wal - datachecksums-disable-checksums-after-xlogctl - datachecksums-disable-checksums-after-controlfile - datachecksums-disable-checksums-before-checkpoint - checkpoint-before-old-wal-removal - datachecksums-disable-checksums-before-barrier-wait); -test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); +#note('TEST OFF/5'); +#@steps = qw(create-checkpoint-initial +# checkpoint-before-redo +# checkpoint-before-xlogctl-checksums +# checkpoint-after-xlogctl-checksums +# checkpoint-before-redo-checksums +# datachecksums-disable-checksums-before-xlog +# datachecksums-disable-checksums-after-xlog +# checkpoint-before-redo-wal +# checkpoint-after-redo-wal +# datachecksums-disable-checksums-after-xlogctl +# datachecksums-disable-checksums-after-controlfile +# datachecksums-disable-checksums-before-checkpoint +# checkpoint-before-old-wal-removal +# datachecksums-disable-checksums-before-barrier-wait); +#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps); note('TEST OFF/6'); @steps = qw(create-checkpoint-initial [text/x-patch] comment-fixes.patch (6.5K, ../../[email protected]/3-comment-fixes.patch) download | inline diff: diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index fecdf0d4b05..462414323a1 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -4807,9 +4807,8 @@ SetDataChecksumsOn(void) /* * The only allowed state transition to "on" is from "inprogress-on" since - * that state ensures that all pages will have data checksums written. No - * such state transition exists, if it does happen it's likely due to a - * programmer error. + * that state ensures that all pages will have data checksums written. Any + * other attempted state transition is likely due to a programmer error. */ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON) { diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c index 9803a0ee2a1..ad4bf4bd2a8 100644 --- a/src/backend/postmaster/auxprocess.c +++ b/src/backend/postmaster/auxprocess.c @@ -88,7 +88,7 @@ AuxiliaryProcessMainCommon(void) * * The postmaster (which is what gets forked into the new child process) * does not handle barriers, therefore it may not have the current value - * of LocalDataChecksumVersion value (it'll have the value read from the + * of LocalDataChecksumState value (it'll have the value read from the * control file, which may be arbitrarily old). * * NB: Even if the postmaster handled barriers, the value might still be diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index 33430147ff2..520eb8db16b 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -20,8 +20,8 @@ * When enabling checksums in an online cluster, data_checksums will be set to * "inprogress-on" which signals that write operations MUST compute and write * the checksum on the data page, but during reading the checksum SHALL NOT be - * verified. This ensures that all objects created during when checksums are - * being enabled will have checksums set, but reads won't fail due to missing or + * verified. This ensures that all objects created while checksums are being + * enabled will have checksums set, but reads won't fail due to missing or * invalid checksums. Invalid checksums can be present in case the cluster had * checksums enabled, then disabled them and updated the page while they were * disabled. @@ -299,10 +299,11 @@ typedef struct DataChecksumsStateStruct bool launcher_running; /* - * Is a worker process currently running? This is set by the worker - * launcher when it starts waiting for a worker process to finish. + * PID of the worker process, if it's currently running, of InvalidPid + * if none. This is set by the worker launcher when it starts waiting + * for a worker process to finish. */ - int worker_pid; + pid_t worker_pid; /* * These fields indicate the target state that the launcher is currently @@ -320,12 +321,8 @@ typedef struct DataChecksumsStateStruct int cost_limit; /* - * Signaling between the launcher and the worker process. - * - * As there is only a single worker, and the launcher won't read these - * until the worker exits, they can be accessed without the need for a - * lock. If multiple workers are supported then this will have to be - * revisited. + * Signaling between the launcher and the worker process. Protected by + * DataChecksumsWorkerLock. */ /* result, set by worker before exiting */ @@ -599,9 +596,9 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op, * * If the launcher is currently busy enabling the checksums, and we want * them disabled (or vice versa), the launcher will notice that at latest - * when it's about to exit, and will loop back process the new request. So - * if the launcher is already running, we don't need to do anything more - * here to abort it. + * when it's about to exit, and will loop back to process the new request. + * So if the launcher is already running, we don't need to do anything + * more here to abort it. * * If you call pg_enable/disable_data_checksums() twice in a row, before * the launcher has had a chance to start up, we still end up launching it @@ -710,10 +707,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg UnlockReleaseBuffer(buf); - /* - * This is the only place where we check if we are asked to abort, the - * abortion will bubble up from here. - */ + /* Check if we are asked to abort, the abortion will bubble up. */ Assert(operation == ENABLE_DATACHECKSUMS); LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED); if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS) @@ -924,8 +918,8 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db) * performed checksum operations exits. A launcher process which is exiting due * to a duplicate started launcher does not need to perform any cleanup and * this function should not be called. Otherwise, we need to clean up the abort - * flag to ensure that processing started again if it was previously aborted - * (note: started again, *not* restarted from where it left off). + * flag to ensure that processing can be started again if it was previously + * aborted (note: started again, *not* restarted from where it left off). */ static void launcher_exit(int code, Datum arg) @@ -1434,7 +1428,7 @@ FreeDatabaseList(List *dblist) * BuildRelationList * Compile a list of relations in the database * - * Returns a list of OIDs for the request relation types. If temp_relations + * Returns a list of OIDs for the requested relation types. If temp_relations * is True then only temporary relations are returned. If temp_relations is * False then non-temporary relations which have data checksums are returned. * If include_shared is True then shared relations are included as well in a diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 2460e550f96..c1457eb34f0 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -773,7 +773,7 @@ InitPostgres(const char *in_dbname, Oid dboid, * * The postmaster (which is what gets forked into the new child process) * does not handle barriers, therefore it may not have the current value - * of LocalDataChecksumVersion value (it'll have the value read from the + * of LocalDataChecksumState value (it'll have the value read from the * control file, which may be arbitrarily old). * * NB: Even if the postmaster handled barriers, the value might still be ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-28 11:28 Daniel Gustafsson <[email protected]> parent: Tomas Vondra <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Daniel Gustafsson @ 2026-05-28 11:28 UTC (permalink / raw) To: Tomas Vondra <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> > On 26 May 2026, at 20:12, Tomas Vondra <[email protected]> wrote: > I suppose this means we should not be updating the checksum state > without emitting the barrier? I think all other places do that. Good catch, it's indeed a bug, any state change must emit a procsignalbarrier to maintain cluster consistency. I ended up writing a test for this very case as well. > I'm still not sure if it really is an issue or just an annoyance, > because I've not been able to find a case where it'd lead to checksum > failures (or obviously incorrect final state after recovery). I've tried to get it to reach an incorrect end state but failed, but I do agree that maybe we need an improved locking protocol around state updates. Need to spend some more time thinking about this. > I still don't understand why this needs DELAY_CHKPT_START ... Having stared at this for some time, and going over old threads, I think this is a mistake. AFAICT though it cannot cause any error, so I'd lean towards erring on the safe side by leaving as is and looking at removing in 20. What do you think? > I also noticed a couple minor comment issues, per attached patch (this > may need pgindent). I ended up splitting this into two, one for the comment fixes and one for the data type change. I propose applying the three patches below to v19 to fix the promotion issue before we wrap beta1. -- Daniel Gustafsson Attachments: [application/octet-stream] 0003-Use-correct-datatype-for-PID.patch (1.2K, ../../[email protected]/2-0003-Use-correct-datatype-for-PID.patch) download | inline diff: From aec5f239b2b09fd877faebb684cd6885b165bd62 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson <[email protected]> Date: Thu, 28 May 2026 12:05:31 +0200 Subject: [PATCH 3/3] Use correct datatype for PID The datachecksums launcher was using int for storing a PID, but the correct type is pid_t (which is defined to be a signed int). Author: Tomas Vondra <[email protected]> Reviewed-by: Daniel Gustafsson <[email protected]> Discussion: https://postgr.es/m/[email protected] --- src/backend/postmaster/datachecksum_state.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index b61674ae957..b578a7cc70e 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -303,7 +303,7 @@ typedef struct DataChecksumsStateStruct * none. This is set by the worker launcher when it starts waiting for a * worker process to finish. */ - int worker_pid; + pid_t worker_pid; /* * These fields indicate the target state that the launcher is currently -- 2.39.3 (Apple Git-146) [application/octet-stream] 0002-Improve-comments-in-online-checksums-code.patch (7.1K, ../../[email protected]/3-0002-Improve-comments-in-online-checksums-code.patch) download | inline diff: From 19d6bc95cf6b8becaa75dbca9b0fdf1d6ce2aef1 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson <[email protected]> Date: Thu, 28 May 2026 12:02:06 +0200 Subject: [PATCH 2/3] Improve comments in online checksums code Spelling fixes and rewording outdated information. Author: Tomas Vondra <[email protected]> Reviewed-by: Daniel Gustafsson <[email protected]> Discussion: https://postgr.es/m/[email protected] --- src/backend/access/transam/xlog.c | 5 ++- src/backend/postmaster/auxprocess.c | 2 +- src/backend/postmaster/datachecksum_state.c | 34 +++++++++------------ src/backend/utils/init/postinit.c | 2 +- 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index b32f54f8402..d69d03b2ef3 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -4807,9 +4807,8 @@ SetDataChecksumsOn(void) /* * The only allowed state transition to "on" is from "inprogress-on" since - * that state ensures that all pages will have data checksums written. No - * such state transition exists, if it does happen it's likely due to a - * programmer error. + * that state ensures that all pages will have data checksums written. Any + * other attempted state transition is likely due to a programmer error. */ if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON) { diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c index 9803a0ee2a1..ad4bf4bd2a8 100644 --- a/src/backend/postmaster/auxprocess.c +++ b/src/backend/postmaster/auxprocess.c @@ -88,7 +88,7 @@ AuxiliaryProcessMainCommon(void) * * The postmaster (which is what gets forked into the new child process) * does not handle barriers, therefore it may not have the current value - * of LocalDataChecksumVersion value (it'll have the value read from the + * of LocalDataChecksumState value (it'll have the value read from the * control file, which may be arbitrarily old). * * NB: Even if the postmaster handled barriers, the value might still be diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c index 33430147ff2..b61674ae957 100644 --- a/src/backend/postmaster/datachecksum_state.c +++ b/src/backend/postmaster/datachecksum_state.c @@ -20,8 +20,8 @@ * When enabling checksums in an online cluster, data_checksums will be set to * "inprogress-on" which signals that write operations MUST compute and write * the checksum on the data page, but during reading the checksum SHALL NOT be - * verified. This ensures that all objects created during when checksums are - * being enabled will have checksums set, but reads won't fail due to missing or + * verified. This ensures that all objects created while checksums are being + * enabled will have checksums set, but reads won't fail due to missing or * invalid checksums. Invalid checksums can be present in case the cluster had * checksums enabled, then disabled them and updated the page while they were * disabled. @@ -299,8 +299,9 @@ typedef struct DataChecksumsStateStruct bool launcher_running; /* - * Is a worker process currently running? This is set by the worker - * launcher when it starts waiting for a worker process to finish. + * PID of the worker process, if it's currently running, of InvalidPid if + * none. This is set by the worker launcher when it starts waiting for a + * worker process to finish. */ int worker_pid; @@ -320,12 +321,8 @@ typedef struct DataChecksumsStateStruct int cost_limit; /* - * Signaling between the launcher and the worker process. - * - * As there is only a single worker, and the launcher won't read these - * until the worker exits, they can be accessed without the need for a - * lock. If multiple workers are supported then this will have to be - * revisited. + * Signaling between the launcher and the worker process. Protected by + * DataChecksumsWorkerLock. */ /* result, set by worker before exiting */ @@ -599,9 +596,9 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op, * * If the launcher is currently busy enabling the checksums, and we want * them disabled (or vice versa), the launcher will notice that at latest - * when it's about to exit, and will loop back process the new request. So - * if the launcher is already running, we don't need to do anything more - * here to abort it. + * when it's about to exit, and will loop back to process the new request. + * So if the launcher is already running, we don't need to do anything + * more here to abort it. * * If you call pg_enable/disable_data_checksums() twice in a row, before * the launcher has had a chance to start up, we still end up launching it @@ -710,10 +707,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg UnlockReleaseBuffer(buf); - /* - * This is the only place where we check if we are asked to abort, the - * abortion will bubble up from here. - */ + /* Check if we are asked to abort, the abortion will bubble up. */ Assert(operation == ENABLE_DATACHECKSUMS); LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED); if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS) @@ -924,8 +918,8 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db) * performed checksum operations exits. A launcher process which is exiting due * to a duplicate started launcher does not need to perform any cleanup and * this function should not be called. Otherwise, we need to clean up the abort - * flag to ensure that processing started again if it was previously aborted - * (note: started again, *not* restarted from where it left off). + * flag to ensure that processing can be started again if it was previously + * aborted (note: started again, *not* restarted from where it left off). */ static void launcher_exit(int code, Datum arg) @@ -1434,7 +1428,7 @@ FreeDatabaseList(List *dblist) * BuildRelationList * Compile a list of relations in the database * - * Returns a list of OIDs for the request relation types. If temp_relations + * Returns a list of OIDs for the requested relation types. If temp_relations * is True then only temporary relations are returned. If temp_relations is * False then non-temporary relations which have data checksums are returned. * If include_shared is True then shared relations are included as well in a diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 2460e550f96..c1457eb34f0 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -773,7 +773,7 @@ InitPostgres(const char *in_dbname, Oid dboid, * * The postmaster (which is what gets forked into the new child process) * does not handle barriers, therefore it may not have the current value - * of LocalDataChecksumVersion value (it'll have the value read from the + * of LocalDataChecksumState value (it'll have the value read from the * control file, which may be arbitrarily old). * * NB: Even if the postmaster handled barriers, the value might still be -- 2.39.3 (Apple Git-146) [application/octet-stream] 0001-Fix-checksum-state-transition-during-promotion.patch (5.7K, ../../[email protected]/4-0001-Fix-checksum-state-transition-during-promotion.patch) download | inline diff: From 37ada0f792137ce33c0346645e792a3e85158a6d Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson <[email protected]> Date: Thu, 28 May 2026 11:45:36 +0200 Subject: [PATCH 1/3] Fix checksum state transition during promotion When a standby is promoted to primary during checksum enabling when the state is inprogress-on, the standby shall revert the state to off since checksums weren't fully enabled at the time of the crash. Consider the following scenario: 1) primary/standby cluster has checksums off 2) primary starts enabling checksums 3) primary moves to inprogress-on 4) standby receives that and moves to inprogress-on too 5) primary crashes 6) standby gets promoted, and does the StartupXLOG thing 7) standby moves from inprogress-on back to off Any processes in the standby need to be informed at step 6 to change state with a procsignalbarrier, else they will stay in inprogress-on while new backends will see the state as off. StartupXLOG failed to emit a procsignalbarrier which caused inconsistent state in the node promoted to primary. Fixed by emitting a procsignalbarrier during promotion, and adding a new test for this scenario. Author: Daniel Gustafsson <[email protected]> Reported-by: Tomas Vondra <[email protected]> Discussion: https://postgr.es/m/[email protected] --- src/backend/access/transam/xlog.c | 5 +- .../test_checksums/t/003_standby_restarts.pl | 58 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index fecdf0d4b05..b32f54f8402 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -6610,6 +6610,7 @@ StartupXLOG(void) SetLocalDataChecksumState(XLogCtl->data_checksum_version); SpinLockRelease(&XLogCtl->info_lck); + EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_OFF); ereport(WARNING, errmsg("enabling data checksums was interrupted"), errhint("Data checksum processing must be manually restarted for checksums to be enabled.")); @@ -6621,7 +6622,7 @@ StartupXLOG(void) * checksums and we can move to off instead of prompting the user to * perform any action. */ - if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF) + else if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF) { XLogChecksums(PG_DATA_CHECKSUM_OFF); @@ -6629,6 +6630,8 @@ StartupXLOG(void) XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF; SetLocalDataChecksumState(XLogCtl->data_checksum_version); SpinLockRelease(&XLogCtl->info_lck); + + EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_OFF); } /* diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl index 5bbf38ed21c..b1f8eedb0bf 100644 --- a/src/test/modules/test_checksums/t/003_standby_restarts.pl +++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl @@ -226,4 +226,62 @@ unlike( qr/page verification failed,.+\d$/m, "no checksum validation errors in standby log"); +# --------------------------------------------------------------------------- +# Test that enforced state transitions during promotion (via StartupXLOG) are +# performed as expected. When the primary crashes during inprogress-on the +# standby should revert to off at promotion. In order to check the transition +# the test will keep an open psql session with the standby during promotion. + +# The cluster is currently broken down from the previous test. Start up the +# primary as primary, disable checksums and create a new standby from that +# state. +$node_standby->clean_node(); +$node_primary->start(); +disable_data_checksums($node_primary, wait => 'off'); + +# Re-create a new streaming standby linking to primary. The replication slot +# name is reused from earlier but a fresh backup is taken +$backup_name = 'my_new_backup'; +$node_primary->backup($backup_name); +$node_standby = PostgreSQL::Test::Cluster->new('standby_restarts_standby'); +$node_standby->init_from_backup($node_primary, $backup_name, + has_streaming => 1); +$node_standby->append_conf( + 'postgresql.conf', qq[ +primary_slot_name = '$slotname' +]); +$node_standby->start; +$node_primary->wait_for_catchup($node_standby, 'replay'); + +# Open a background psql connection on the primary and inject a barrier to +# block progress on to keep the state from advancing past inprogress-on +my $node_primary_bpsql = $node_primary->background_psql('postgres'); +$node_primary_bpsql->query_safe('CREATE TEMPORARY TABLE tt (a integer);'); +# Also open a background psql connection to the standby to make sure we have +# an active backend during promotion. +my $node_standby_bpsql = $node_standby->background_psql('postgres'); + +# Start to enable checksums and wait until both primary and standby have moved +# to the inprogress-on state. Processing will block here as the temporary rel +# barrier will block the primary from finishing. +enable_data_checksums($node_primary, wait => 'inprogress-on'); +$node_primary->wait_for_catchup($node_standby, 'replay'); +test_checksum_state($node_standby, 'inprogress-on'); + +# Crash the primary before checksums are enabled and promote the standby. The +# new primary node will now revert the state of 'off' since checksums weren't +# fully enabled during the crash. +$node_primary->teardown_node(); +$node_standby->promote; +wait_for_checksum_state($node_standby, 'off'); + +# Ensure that the any backend which was active befire, and during, promotion +# sees the new state. +$result = $node_standby_bpsql->query_safe("SHOW data_checksums;"); +is($result, 'off', + 'ensure checksums are set to off after promotion during inprogress-on'); + +$node_standby_bpsql->quit; +$node_standby->stop; + done_testing(); -- 2.39.3 (Apple Git-146) ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-28 11:51 Tomas Vondra <[email protected]> parent: Daniel Gustafsson <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Tomas Vondra @ 2026-05-28 11:51 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> On 5/28/26 13:28, Daniel Gustafsson wrote: >> On 26 May 2026, at 20:12, Tomas Vondra <[email protected]> wrote: > >> I suppose this means we should not be updating the checksum state >> without emitting the barrier? I think all other places do that. > > Good catch, it's indeed a bug, any state change must emit a procsignalbarrier > to maintain cluster consistency. I ended up writing a test for this very case > as well. > Good. >> I'm still not sure if it really is an issue or just an annoyance, >> because I've not been able to find a case where it'd lead to checksum >> failures (or obviously incorrect final state after recovery). > > I've tried to get it to reach an incorrect end state but failed, but I do agree > that maybe we need an improved locking protocol around state updates. Need to > spend some more time thinking about this. > OK >> I still don't understand why this needs DELAY_CHKPT_START ... > > Having stared at this for some time, and going over old threads, I think this > is a mistake. AFAICT though it cannot cause any error, so I'd lean towards > erring on the safe side by leaving as is and looking at removing in 20. What > do you think? > I'd probably try to fix this for 19, otherwise it may be confusing people looking at the code in the future. We're still months from 19 getting released. Ofc, maybe I'm underestimating the risk. >> I also noticed a couple minor comment issues, per attached patch (this >> may need pgindent). > > I ended up splitting this into two, one for the comment fixes and one for the > data type change. > > I propose applying the three patches below to v19 to fix the promotion issue > before we wrap beta1. > WFM > -- > Daniel Gustafsson > -- Tomas Vondra ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-29 20:08 Daniel Gustafsson <[email protected]> parent: Tomas Vondra <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Daniel Gustafsson @ 2026-05-29 20:08 UTC (permalink / raw) To: Tomas Vondra <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> > On 28 May 2026, at 13:51, Tomas Vondra <[email protected]> wrote: > > On 5/28/26 13:28, Daniel Gustafsson wrote: >>> On 26 May 2026, at 20:12, Tomas Vondra <[email protected]> wrote: >> >>> I suppose this means we should not be updating the checksum state >>> without emitting the barrier? I think all other places do that. >> >> Good catch, it's indeed a bug, any state change must emit a procsignalbarrier >> to maintain cluster consistency. I ended up writing a test for this very case >> as well. > > Good. I've pushed this now, along with your other findings, ahead of the beta1 deadline, buildfarm seems happy so far. >>> I still don't understand why this needs DELAY_CHKPT_START ... >> >> Having stared at this for some time, and going over old threads, I think this >> is a mistake. AFAICT though it cannot cause any error, so I'd lean towards >> erring on the safe side by leaving as is and looking at removing in 20. What >> do you think? >> > > I'd probably try to fix this for 19, otherwise it may be confusing > people looking at the code in the future. We're still months from 19 > getting released. Ofc, maybe I'm underestimating the risk. You're probably right. Once beta1 is out I'll work on getting this fixed. -- Daniel Gustafsson ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-05-29 20:27 Tomas Vondra <[email protected]> parent: Daniel Gustafsson <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Tomas Vondra @ 2026-05-29 20:27 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Ayush Tiwari <[email protected]>; PostgreSQL Hackers <[email protected]>; Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; Bernd Helmle <[email protected]>; Michael Paquier <[email protected]>; Michael Banck <[email protected]> On 5/29/26 22:08, Daniel Gustafsson wrote: >> On 28 May 2026, at 13:51, Tomas Vondra <[email protected]> wrote: >> >> On 5/28/26 13:28, Daniel Gustafsson wrote: >>>> On 26 May 2026, at 20:12, Tomas Vondra <[email protected]> wrote: >>> >>>> I suppose this means we should not be updating the checksum state >>>> without emitting the barrier? I think all other places do that. >>> >>> Good catch, it's indeed a bug, any state change must emit a procsignalbarrier >>> to maintain cluster consistency. I ended up writing a test for this very case >>> as well. >> >> Good. > > I've pushed this now, along with your other findings, ahead of the beta1 > deadline, buildfarm seems happy so far. > Thanks! >>>> I still don't understand why this needs DELAY_CHKPT_START ... >>> >>> Having stared at this for some time, and going over old threads, I think this >>> is a mistake. AFAICT though it cannot cause any error, so I'd lean towards >>> erring on the safe side by leaving as is and looking at removing in 20. What >>> do you think? >>> >> >> I'd probably try to fix this for 19, otherwise it may be confusing >> people looking at the code in the future. We're still months from 19 >> getting released. Ofc, maybe I'm underestimating the risk. > > You're probably right. Once beta1 is out I'll work on getting this fixed. > +1 regards -- Tomas Vondra ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-07-02 05:06 Palak Chaturvedi <[email protected]> parent: Tomas Vondra <[email protected]> 2 siblings, 1 reply; 24+ messages in thread From: Palak Chaturvedi @ 2026-07-02 05:06 UTC (permalink / raw) To: Tomas Vondra <[email protected]>; pgsql-hackers Hi Tomas, Thanks for these test patches. They are quite useful. I've been going through them because we think they'll also help with the TAP tests for the online shared_buffers resize work , where I'm ending up reusing several of the same helpers (attach_injection_points, background_rw_pgbench, and a couple of others). Would you be open to lifting these helpers out into a shared Perl module (for example under src/test/perl/PostgreSQL/Test/…) so both test suites can consume them instead of each copy-pasting? I'm happy to prepare a small prep patch on top of your latest version if that sounds reasonable to you. Thanks, Palak On Mon, 4 May 2026 at 18:47, Tomas Vondra <[email protected]> wrote: > Hi, > > Thanks for getting this feature pushed, and for resolving the failures > reported since the feature freeze. I consider this to be an important > improvement, not just for the feature itself, but also because of all > the useful infrastructure it added. > > Attached is a refined version of the TAP tests already posted by Daniel > some time ago [1]. Unfortunately, that .txt did not apply cleanly for > some reason, so here's a better version. > > I found these tests quite useful when reasoning about how the patch > behaves in concurrent environment (e.g. with multiple sessions > triggering checksum enable/disable, or with a checkpoint, crashes, etc). > > At this point all the tests pass, but there are a couple cases with > correct but slightly surprising behavior, worth discussing. Which is > what this e-mail is going to be about. > > I'll explain what the TAP tests aim to do first, and then discuss the > slightly surprising behavior. > > It's not meant for inclusion into PG19, at least not in this shape - I > wrote those TAP tests while investigating some of the earlier failures > and/or when wondering about behavior in various situations (sequence of > concurrent steps, race conditions, ...). So it's more of an exhaustive, > and the tests are somewhat redundant (N+1 is often just (N + some small > tweak)). > > I can imagine distilling it into a tiny subset, and adding that. But > that's up to discussion. But that's for later. > > > Let me briefly explain what the various TAP tests aim to do. From the > very beginning, my main concern regarding this patch was race conditions > when updating the shared state about effective data_checksum_version. > Because the state is effectively split into about three or four places: > > * LocalDataChecksumVersion (local cache) > * XLogCtl->data_checksum_version (XLogCtl->info_lck) > * ControlFile->data_checksum_version (ControlFileLock) > * state in control file on disk > > These pieces are protected by different locks, the protocol for updating > and/or reading the various flags is not trivial (and some of the fixed > issues were due to ControlFile->data_checksum_version being updated from > a place that shouldn't have). > > So the primary goal of the TAP tests was to check for race conditions by > leveraging injection points to step through concurrent processes in a > deterministic way. The first couple patches (0001-0004) add debug > logging and injection points into a lot of places. And by "a lot" I mean > ~80 new injection points, which is about the number of injection points > we have in master now. Anyway, this allows stepping through concurrent > checksum changes, and also checksum change vs. checkpointer. > > Then come the actual TAP tests: > > 1) 0005-TAP-10-concurrent-checksum-changes.patch > > Two concurrent checksum changes. The first one gets paused at an > injection point, then the second one gets initiated. > > 2) 0006-TAP-11-concurrency-with-checkpoints.patch > > A checksum change + checkpoint. The change gets paused at an injection > point, a synchronous checkpoint is performed. > > 3) 0007-TAP-12-crashes-at-injection-points.patch > > Similar to 0006, but with a crash + recovery. A checksum change gets > paused at an injection point, a synchronous checkpoint is performed. The > changes gets wpken up and either completes, or pauses on a different > injection point. A restart/crash happens. > > 4) 0008-TAP-13-concurrency-with-checkpoint-REDO.patch > > Similar to 0007, but the checkpoint is not synchronous - happens in the > background, so that the TAP can step through both sides and interleave > them in an arbitrary way. This matters because the checksum change > updates the different state pieces (XlogCtl/ControlFile), while the > checkpointer reads them to record initial state for REDO etc. > > 5) 0009-TAP-14-checkpoints-with-crashes.patch > > Similar to 0008, except that the steps are more fine grained, and > focused on two particular cases with surprisingly different final state. > > > AFAIK everything works as expected, except for two cases in the "TAP > 012" test. One for the "enabling" direction, one for the "disabling" > direction. I'm going to discuss the "enabling" direction, I believe the > other case is just a mirror with the same root cause. > > The TAP 012 tests checksum change with a concurrent checkpoint, followed > by a crash, and tests the final state. It pauses the change at an > injection point, does a checkpoint, proceeds to the next injection > point, crashes and does recovery. The expectation is that the final > state "flips" at some injection point, once it gets further enough, and > stays there. But what actually happens is this: > > a) test_checksum_transition( > 'disabled', 'enable', undef, > 'datachecksums-enable-inprogress-checksums-end', > 'datachecksums-enable-checksums-start', > 'off'); > > b) test_checksum_transition( > 'disabled', 'enable', undef, > 'datachecksums-enable-checksums-start', > 'datachecksums-enable-checksums-after-xlog', > 'on'); > > c) test_checksum_transition( > 'disabled', 'enable', 'datachecksums-enable-checksums-start', > 'datachecksums-enable-checksums-after-xlogs', > 'datachecksums-enable-checksums-after-xlogctl', > 'off'); > > This says that if the checkpoint happens after > 'datachecksums-enable-inprogress-checksums-end' or after > 'datachecksums-enable-checksums-after-xlog', we end up with 'off' (i.e. > enabling checksums fails). > > But if the checkpoint happens after > 'datachecksums-enable-checksums-start', we end up with "on" (after > recovery). > > This is a bit surprising, because that injection point is before > 'datachecksums-enable-checksums-after-xlog'. So the enabling process > gets further and further, but the final state flips off -> on -> off, > contradicting the expectation that it changes once. > > I haven't quite wrapped my head around it yet, but my understanding is > this is due to a race condition between the checksum launcher (writing > XLOG2_CHECKSUMS and updating the shmem state), and the checkpointer > (reading the shmem state and generating REDO). > > The launcher does this sequence of steps: > > 1) write XLOG2_CHECKSUMS with new state > 2) update XLogCtl->data_checksum_version > 3) update ControlFile->data_checksum_version > 4) UpdateControlFile() > 5) emits barrier > > while the checkpointer (CreateCheckPoint) does this: > > A) read XLogCtl->data_checksum_version (while holding insert locks) > B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version) > C) UpdateControlFile() > > The outcome depends on how exactly these two sequences interleave. For > example, this can happen: > > 1) write XLOG2_CHECKSUMS with new state > A) read XLogCtl->data_checksum_version (while holding insert locks) > B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version) > C) UpdateControlFile() > 2) update XLogCtl->data_checksum_version > 3) update ControlFile->data_checksum_version > 4) UpdateControlFile() > 5) emits barrier > > Which means the XLOG_CHECKPOINT_REDO will be after XLOG2_CHECKSUMS (and > so redo won't see it), but the checkpoint will still get the old > checksum state from XLogCtl. And so the outcome is "off", per case (c). > > But it can also happen what case (b) does: > > A) read XLogCtl->data_checksum_version (while holding insert locks) > B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version) > C) UpdateControlFile() > 1) write XLOG2_CHECKSUMS with new state > 2) update XLogCtl->data_checksum_version > 3) update ControlFile->data_checksum_version > 4) UpdateControlFile() > 5) emits barrier > > In which case the REDO will have the old state, but the recovery will > read the XLOG2_CHECKSUMS, and so end up with "on". > > This is the root cause of the surprising behavior in TAP 012, I think. > > I attempted to trigger these race conditions in TAP 013, but without > much success. In the end I realized it probably needs more control, > waiting for the other process to hit the next injection point before > unpausing the current one. TAP 014 does that, and it shows that with the > right interleaving of steps the (c) case can end up with both "on" and > "off" final state. > > As I said, I don't claim I fully understand this yet. But I wouldn't > call this "bug" - AFAICS it won't produce an incorrect final state (I > haven't seen any such cases). > > Still, I wonder if there's a potential issue I failed to notice. > > > The other question I had when looking at this (concurrency with > checkpoints) is what we get by doing > > MyProc->delayChkptFlags |= DELAY_CHKPT_START; > > whenever updating the state in SetDataChecksums... functions. Because > the only thing that guarantees is the updates happen on one side of the > checkpoint record. What does that give us, actually? > > It does not seem to prevent this surprising behavior, and it does not > say the XLOG2_CHECKSUMS happens before/after the XLOG_CHECKPOINT_REDO. > > > regards > > [1] > > https://www.postgresql.org/message-id/9197F930-DDEB-4CAC-82A2-16FEC715CCE8%40yesql.se > > -- > Tomas Vondra > ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Changing the state of data checksums in a running cluster @ 2026-07-02 08:29 Tomas Vondra <[email protected]> parent: Palak Chaturvedi <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Tomas Vondra @ 2026-07-02 08:29 UTC (permalink / raw) To: Palak Chaturvedi <[email protected]>; pgsql-hackers On 7/2/26 07:06, Palak Chaturvedi wrote: > Hi Tomas, > > Thanks for these test patches. They are quite useful. I've been going > through them because we think they'll also help with the TAP tests for > the online shared_buffers resize work , where I'm ending up reusing > several of the same helpers (attach_injection_points, > background_rw_pgbench, and a couple of others). > Would you be open to lifting these helpers out into a shared Perl module > (for example under src/test/perl/PostgreSQL/Test/…) so both test suites > can consume them instead of each copy-pasting? I'm happy to prepare a > small prep patch on top of your latest version if that sounds reasonable > to you. > I suppose you mean the functions for managing injection points (waiting, waking, attachin/detaching, ...)? Yes, it might be useful to have that in a Perl module, so feel free to create one. regards -- Tomas Vondra ^ permalink raw reply [nested|flat] 24+ messages in thread
end of thread, other threads:[~2026-07-02 08:29 UTC | newest] Thread overview: 24+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2019-07-05 14:24 [PATCH 15/17] Make pg_upgrade aware of encryption. Antonin Houska <[email protected]> 2023-09-12 05:22 [PATCH v6 5/7] Row pattern recognition patch (docs). Tatsuo Ishii <[email protected]> 2026-01-22 08:23 [PATCH 1/6] Add REPACK command Antonin Houska <[email protected]> 2026-05-01 16:57 Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]> 2026-05-01 21:13 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]> 2026-05-04 13:16 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]> 2026-05-05 07:43 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]> 2026-05-05 13:46 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]> 2026-05-05 14:24 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]> 2026-05-05 15:21 ` Re: Changing the state of data checksums in a running cluster Ayush Tiwari <[email protected]> 2026-05-05 19:08 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]> 2026-05-05 19:18 ` Re: Changing the state of data checksums in a running cluster Ayush Tiwari <[email protected]> 2026-05-05 19:45 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]> 2026-05-05 20:05 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]> 2026-05-05 21:01 ` Re: Changing the state of data checksums in a running cluster Ayush Tiwari <[email protected]> 2026-05-06 15:22 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]> 2026-05-05 19:19 ` Re: Changing the state of data checksums in a running cluster SATYANARAYANA NARLAPURAM <[email protected]> 2026-05-26 18:12 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]> 2026-05-28 11:28 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]> 2026-05-28 11:51 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]> 2026-05-29 20:08 ` Re: Changing the state of data checksums in a running cluster Daniel Gustafsson <[email protected]> 2026-05-29 20:27 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[email protected]> 2026-07-02 05:06 ` Re: Changing the state of data checksums in a running cluster Palak Chaturvedi <[email protected]> 2026-07-02 08:29 ` Re: Changing the state of data checksums in a running cluster Tomas Vondra <[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