diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml index 61c12975e4ad..16b50b5a4df0 100644 --- a/doc/src/sgml/ref/pg_verifybackup.sgml +++ b/doc/src/sgml/ref/pg_verifybackup.sgml @@ -36,10 +36,7 @@ PostgreSQL documentation backup_manifest generated by the server at the time of the backup. The backup may be stored either in the "plain" or the "tar" format; this includes tar-format backups compressed with any algorithm - supported by pg_basebackup. However, at present, - WAL verification is supported only for plain-format - backups. Therefore, if the backup is stored in tar-format, the - -n, --no-parse-wal option should be used. + supported by pg_basebackup. @@ -261,7 +258,7 @@ PostgreSQL documentation - + Try to parse WAL files stored in the specified directory, rather than diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml index ce23add5577e..c1afb4097b55 100644 --- a/doc/src/sgml/ref/pg_waldump.sgml +++ b/doc/src/sgml/ref/pg_waldump.sgml @@ -141,13 +141,20 @@ PostgreSQL documentation - Specifies a directory to search for WAL segment files or a - directory with a pg_wal subdirectory that + Specifies a tar archive or a directory to search for WAL segment files + or a directory with a pg_wal subdirectory that contains such files. The default is to search in the current directory, the pg_wal subdirectory of the current directory, and the pg_wal subdirectory of PGDATA. + + If a tar archive is provided and its WAL segment files are not in + sequential order, those files will be written temporarily. These files + will be created inside the directory specified by the TMPDIR + environment variable if it is set; otherwise, the temporary files will + be created within the same directory as the tar archive itself. + diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c index 8d5befa947f5..6915fc7f28e4 100644 --- a/src/bin/pg_verifybackup/pg_verifybackup.c +++ b/src/bin/pg_verifybackup/pg_verifybackup.c @@ -74,7 +74,9 @@ pg_noreturn static void report_manifest_error(JsonManifestParseContext *context, const char *fmt,...) pg_attribute_printf(2, 3); -static void verify_tar_backup(verifier_context *context, DIR *dir); +static void verify_tar_backup(verifier_context *context, DIR *dir, + char **base_archive_path, + char **wal_archive_path); static void verify_plain_backup_directory(verifier_context *context, char *relpath, char *fullpath, DIR *dir); @@ -83,7 +85,9 @@ static void verify_plain_backup_file(verifier_context *context, char *relpath, static void verify_control_file(const char *controlpath, uint64 manifest_system_identifier); static void precheck_tar_backup_file(verifier_context *context, char *relpath, - char *fullpath, SimplePtrList *tarfiles); + char *fullpath, SimplePtrList *tarfiles, + char **base_archive_path, + char **wal_archive_path); static void verify_tar_file(verifier_context *context, char *relpath, char *fullpath, astreamer *streamer); static void report_extra_backup_files(verifier_context *context); @@ -93,7 +97,7 @@ static void verify_file_checksum(verifier_context *context, uint8 *buffer); static void parse_required_wal(verifier_context *context, char *pg_waldump_path, - char *wal_directory); + char *wal_path); static astreamer *create_archive_verifier(verifier_context *context, char *archive_name, Oid tblspc_oid, @@ -126,7 +130,7 @@ main(int argc, char **argv) {"progress", no_argument, NULL, 'P'}, {"quiet", no_argument, NULL, 'q'}, {"skip-checksums", no_argument, NULL, 's'}, - {"wal-directory", required_argument, NULL, 'w'}, + {"wal-path", required_argument, NULL, 'w'}, {NULL, 0, NULL, 0} }; @@ -135,7 +139,9 @@ main(int argc, char **argv) char *manifest_path = NULL; bool no_parse_wal = false; bool quiet = false; - char *wal_directory = NULL; + char *wal_path = NULL; + char *base_archive_path = NULL; + char *wal_archive_path = NULL; char *pg_waldump_path = NULL; DIR *dir; @@ -221,8 +227,8 @@ main(int argc, char **argv) context.skip_checksums = true; break; case 'w': - wal_directory = pstrdup(optarg); - canonicalize_path(wal_directory); + wal_path = pstrdup(optarg); + canonicalize_path(wal_path); break; default: /* getopt_long already emitted a complaint */ @@ -285,10 +291,6 @@ main(int argc, char **argv) manifest_path = psprintf("%s/backup_manifest", context.backup_directory); - /* By default, look for the WAL in the backup directory, too. */ - if (wal_directory == NULL) - wal_directory = psprintf("%s/pg_wal", context.backup_directory); - /* * Try to read the manifest. We treat any errors encountered while parsing * the manifest as fatal; there doesn't seem to be much point in trying to @@ -331,17 +333,6 @@ main(int argc, char **argv) pfree(path); } - /* - * XXX: In the future, we should consider enhancing pg_waldump to read WAL - * files from an archive. - */ - if (!no_parse_wal && context.format == 't') - { - pg_log_error("pg_waldump cannot read tar files"); - pg_log_error_hint("You must use -n/--no-parse-wal when verifying a tar-format backup."); - exit(1); - } - /* * Perform the appropriate type of verification appropriate based on the * backup format. This will close 'dir'. @@ -350,7 +341,7 @@ main(int argc, char **argv) verify_plain_backup_directory(&context, NULL, context.backup_directory, dir); else - verify_tar_backup(&context, dir); + verify_tar_backup(&context, dir, &base_archive_path, &wal_archive_path); /* * The "matched" flag should now be set on every entry in the hash table. @@ -368,12 +359,35 @@ main(int argc, char **argv) if (context.format == 'p' && !context.skip_checksums) verify_backup_checksums(&context); + /* + * By default, WAL files are expected to be found in the backup directory + * for plain-format backups. In the case of tar-format backups, if a + * separate WAL archive is not found, the WAL files are most likely + * included within the main data directory archive. + */ + if (wal_path == NULL) + { + if (context.format == 'p') + wal_path = psprintf("%s/pg_wal", context.backup_directory); + else if (wal_archive_path) + wal_path = wal_archive_path; + else if (base_archive_path) + wal_path = base_archive_path; + else + { + pg_log_error("wal archive not found"); + pg_log_error_hint("Specify the correct path using the option -w/--wal-path." + "Or you must use -n/--no-parse-wal when verifying a tar-format backup."); + exit(1); + } + } + /* * Try to parse the required ranges of WAL records, unless we were told * not to do so. */ if (!no_parse_wal) - parse_required_wal(&context, pg_waldump_path, wal_directory); + parse_required_wal(&context, pg_waldump_path, wal_path); /* * If everything looks OK, tell the user this, unless we were asked to @@ -787,7 +801,8 @@ verify_control_file(const char *controlpath, uint64 manifest_system_identifier) * close when we're done with it. */ static void -verify_tar_backup(verifier_context *context, DIR *dir) +verify_tar_backup(verifier_context *context, DIR *dir, char **base_archive_path, + char **wal_archive_path) { struct dirent *dirent; SimplePtrList tarfiles = {NULL, NULL}; @@ -816,7 +831,8 @@ verify_tar_backup(verifier_context *context, DIR *dir) char *fullpath; fullpath = psprintf("%s/%s", context->backup_directory, filename); - precheck_tar_backup_file(context, filename, fullpath, &tarfiles); + precheck_tar_backup_file(context, filename, fullpath, &tarfiles, + base_archive_path, wal_archive_path); pfree(fullpath); } } @@ -875,11 +891,13 @@ verify_tar_backup(verifier_context *context, DIR *dir) * * The arguments to this function are mostly the same as the * verify_plain_backup_file. The additional argument outputs a list of valid - * tar files. + * tar files, along with the full paths to the main archive and the WAL + * directory archive. */ static void precheck_tar_backup_file(verifier_context *context, char *relpath, - char *fullpath, SimplePtrList *tarfiles) + char *fullpath, SimplePtrList *tarfiles, + char **base_archive_path, char **wal_archive_path) { struct stat sb; Oid tblspc_oid = InvalidOid; @@ -918,9 +936,17 @@ precheck_tar_backup_file(verifier_context *context, char *relpath, * extension such as .gz, .lz4, or .zst. */ if (strncmp("base", relpath, 4) == 0) + { suffix = relpath + 4; + + *base_archive_path = pstrdup(fullpath); + } else if (strncmp("pg_wal", relpath, 6) == 0) + { suffix = relpath + 6; + + *wal_archive_path = pstrdup(fullpath); + } else { /* Expected a .tar file here. */ @@ -1198,7 +1224,7 @@ verify_file_checksum(verifier_context *context, manifest_file *m, */ static void parse_required_wal(verifier_context *context, char *pg_waldump_path, - char *wal_directory) + char *wal_path) { manifest_data *manifest = context->manifest; manifest_wal_range *this_wal_range = manifest->first_wal_range; @@ -1208,7 +1234,7 @@ parse_required_wal(verifier_context *context, char *pg_waldump_path, char *pg_waldump_cmd; pg_waldump_cmd = psprintf("\"%s\" --quiet --path=\"%s\" --timeline=%u --start=%X/%08X --end=%X/%08X\n", - pg_waldump_path, wal_directory, this_wal_range->tli, + pg_waldump_path, wal_path, this_wal_range->tli, LSN_FORMAT_ARGS(this_wal_range->start_lsn), LSN_FORMAT_ARGS(this_wal_range->end_lsn)); fflush(NULL); @@ -1376,7 +1402,7 @@ usage(void) printf(_(" -P, --progress show progress information\n")); printf(_(" -q, --quiet do not print any output, except for errors\n")); printf(_(" -s, --skip-checksums skip checksum verification\n")); - printf(_(" -w, --wal-directory=PATH use specified path for WAL files\n")); + printf(_(" -w, --wal-path=PATH use specified path for WAL files\n")); printf(_(" -V, --version output version information, then exit\n")); printf(_(" -?, --help show this help, then exit\n")); printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT); diff --git a/src/bin/pg_verifybackup/po/de.po b/src/bin/pg_verifybackup/po/de.po index a9e24931100c..9b5cd5898cfa 100644 --- a/src/bin/pg_verifybackup/po/de.po +++ b/src/bin/pg_verifybackup/po/de.po @@ -785,8 +785,8 @@ msgstr " -s, --skip-checksums Überprüfung der Prüfsummen überspringe #: pg_verifybackup.c:1379 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=PFAD angegebenen Pfad für WAL-Dateien verwenden\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=PFAD angegebenen Pfad für WAL-Dateien verwenden\n" #: pg_verifybackup.c:1380 #, c-format diff --git a/src/bin/pg_verifybackup/po/el.po b/src/bin/pg_verifybackup/po/el.po index 3e3f20c67c5f..81442f51c17c 100644 --- a/src/bin/pg_verifybackup/po/el.po +++ b/src/bin/pg_verifybackup/po/el.po @@ -494,8 +494,8 @@ msgstr " -s, --skip-checksums παράκαμψε την επαλήθευ #: pg_verifybackup.c:992 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=PATH χρησιμοποίησε την καθορισμένη διαδρομή για αρχεία WAL\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=PATH χρησιμοποίησε την καθορισμένη διαδρομή για αρχεία WAL\n" #: pg_verifybackup.c:993 #, c-format diff --git a/src/bin/pg_verifybackup/po/es.po b/src/bin/pg_verifybackup/po/es.po index 0cb958f34488..7f729fa35baf 100644 --- a/src/bin/pg_verifybackup/po/es.po +++ b/src/bin/pg_verifybackup/po/es.po @@ -495,8 +495,8 @@ msgstr " -s, --skip-checksums omitir la verificación de la suma de comp #: pg_verifybackup.c:992 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=PATH utilizar la ruta especificada para los archivos WAL\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=PATH utilizar la ruta especificada para los archivos WAL\n" #: pg_verifybackup.c:993 #, c-format diff --git a/src/bin/pg_verifybackup/po/fr.po b/src/bin/pg_verifybackup/po/fr.po index da8c72f64271..09937966fa71 100644 --- a/src/bin/pg_verifybackup/po/fr.po +++ b/src/bin/pg_verifybackup/po/fr.po @@ -498,8 +498,8 @@ msgstr " -s, --skip-checksums ignore la vérification des sommes de cont #: pg_verifybackup.c:992 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=CHEMIN utilise le chemin spécifié pour les fichiers WAL\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=CHEMIN utilise le chemin spécifié pour les fichiers WAL\n" #: pg_verifybackup.c:993 #, c-format diff --git a/src/bin/pg_verifybackup/po/it.po b/src/bin/pg_verifybackup/po/it.po index 317b0b71e7f3..4da68d0074e9 100644 --- a/src/bin/pg_verifybackup/po/it.po +++ b/src/bin/pg_verifybackup/po/it.po @@ -472,8 +472,8 @@ msgstr " -s, --skip-checksums salta la verifica del checksum\n" #: pg_verifybackup.c:911 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=PATH usa il percorso specificato per i file WAL\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=PATH usa il percorso specificato per i file WAL\n" #: pg_verifybackup.c:912 #, c-format diff --git a/src/bin/pg_verifybackup/po/ja.po b/src/bin/pg_verifybackup/po/ja.po index c910fb236cc6..a948959b54ff 100644 --- a/src/bin/pg_verifybackup/po/ja.po +++ b/src/bin/pg_verifybackup/po/ja.po @@ -672,8 +672,8 @@ msgstr " -s, --skip-checksums チェックサム検証をスキップ\n" #: pg_verifybackup.c:1379 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=PATH WALファイルに指定したパスを使用する\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=PATH WALファイルに指定したパスを使用する\n" #: pg_verifybackup.c:1380 #, c-format diff --git a/src/bin/pg_verifybackup/po/ka.po b/src/bin/pg_verifybackup/po/ka.po index 982751984c78..ef2799316a87 100644 --- a/src/bin/pg_verifybackup/po/ka.po +++ b/src/bin/pg_verifybackup/po/ka.po @@ -784,8 +784,8 @@ msgstr " -s, --skip-checksums საკონტროლო ჯამ #: pg_verifybackup.c:1379 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=ბილიკი WAL ფაილებისთვის მითითებული ბილიკის გამოყენება\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=ბილიკი WAL ფაილებისთვის მითითებული ბილიკის გამოყენება\n" #: pg_verifybackup.c:1380 #, c-format diff --git a/src/bin/pg_verifybackup/po/ko.po b/src/bin/pg_verifybackup/po/ko.po index acdc3da5e02c..eaf91ef1e982 100644 --- a/src/bin/pg_verifybackup/po/ko.po +++ b/src/bin/pg_verifybackup/po/ko.po @@ -501,8 +501,8 @@ msgstr " -s, --skip-checksums 체크섬 검사 건너뜀\n" #: pg_verifybackup.c:992 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=경로 WAL 파일이 있는 경로 지정\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=경로 WAL 파일이 있는 경로 지정\n" #: pg_verifybackup.c:993 #, c-format diff --git a/src/bin/pg_verifybackup/po/ru.po b/src/bin/pg_verifybackup/po/ru.po index 64005feedfdc..7fb0e5ab1f6a 100644 --- a/src/bin/pg_verifybackup/po/ru.po +++ b/src/bin/pg_verifybackup/po/ru.po @@ -507,9 +507,9 @@ msgstr " -s, --skip-checksums пропустить проверку ко #: pg_verifybackup.c:992 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" msgstr "" -" -w, --wal-directory=ПУТЬ использовать заданный путь к файлам WAL\n" +" -w, --wal-path=ПУТЬ использовать заданный путь к файлам WAL\n" #: pg_verifybackup.c:993 #, c-format diff --git a/src/bin/pg_verifybackup/po/sv.po b/src/bin/pg_verifybackup/po/sv.po index 17240feeb5c2..97125838e8c7 100644 --- a/src/bin/pg_verifybackup/po/sv.po +++ b/src/bin/pg_verifybackup/po/sv.po @@ -492,8 +492,8 @@ msgstr " -s, --skip-checksums hoppa över verifiering av kontrollsummor\ #: pg_verifybackup.c:992 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=SÖKVÄG använd denna sökväg till WAL-filer\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=SÖKVÄG använd denna sökväg till WAL-filer\n" #: pg_verifybackup.c:993 #, c-format diff --git a/src/bin/pg_verifybackup/po/uk.po b/src/bin/pg_verifybackup/po/uk.po index 034b97642323..63f8041ab388 100644 --- a/src/bin/pg_verifybackup/po/uk.po +++ b/src/bin/pg_verifybackup/po/uk.po @@ -484,8 +484,8 @@ msgstr " -s, --skip-checksums не перевіряти контрольні с #: pg_verifybackup.c:992 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=PATH використовувати вказаний шлях для файлів WAL\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=PATH використовувати вказаний шлях для файлів WAL\n" #: pg_verifybackup.c:993 #, c-format diff --git a/src/bin/pg_verifybackup/po/zh_CN.po b/src/bin/pg_verifybackup/po/zh_CN.po index b7d97c8976db..fb6fcae8b821 100644 --- a/src/bin/pg_verifybackup/po/zh_CN.po +++ b/src/bin/pg_verifybackup/po/zh_CN.po @@ -465,8 +465,8 @@ msgstr " -s, --skip-checksums 跳过校验和验证\n" #: pg_verifybackup.c:919 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=PATH 对WAL文件使用指定路径\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=PATH 对WAL文件使用指定路径\n" #: pg_verifybackup.c:920 #, c-format diff --git a/src/bin/pg_verifybackup/po/zh_TW.po b/src/bin/pg_verifybackup/po/zh_TW.po index c1b710b0a366..568f972b0bbb 100644 --- a/src/bin/pg_verifybackup/po/zh_TW.po +++ b/src/bin/pg_verifybackup/po/zh_TW.po @@ -555,8 +555,8 @@ msgstr " -s, --skip-checksums 跳過檢查碼驗證\n" #: pg_verifybackup.c:992 #, c-format -msgid " -w, --wal-directory=PATH use specified path for WAL files\n" -msgstr " -w, --wal-directory=PATH 用指定的路徑存放 WAL 檔\n" +msgid " -w, --wal-path=PATH use specified path for WAL files\n" +msgstr " -w, --wal-path=PATH 用指定的路徑存放 WAL 檔\n" #: pg_verifybackup.c:993 #, c-format diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl index ae16c11bc4dd..4f284a9e8285 100644 --- a/src/bin/pg_verifybackup/t/002_algorithm.pl +++ b/src/bin/pg_verifybackup/t/002_algorithm.pl @@ -30,10 +30,6 @@ sub test_checksums { # Add switch to get a tar-format backup push @backup, ('--format' => 'tar'); - - # Add switch to skip WAL verification, which is not yet supported for - # tar-format backups - push @verify, ('--no-parse-wal'); } # A backup with a bogus algorithm should fail. diff --git a/src/bin/pg_verifybackup/t/003_corruption.pl b/src/bin/pg_verifybackup/t/003_corruption.pl index 1dd60f709cf6..f1ebdbb46b4b 100644 --- a/src/bin/pg_verifybackup/t/003_corruption.pl +++ b/src/bin/pg_verifybackup/t/003_corruption.pl @@ -193,10 +193,8 @@ command_ok([ $tar, '-cf' => "$tar_backup_path/base.tar", '.' ]); chdir($cwd) || die "chdir: $!"; - # Now check that the backup no longer verifies. We must use -n - # here, because pg_waldump can't yet read WAL from a tarfile. command_fails_like( - [ 'pg_verifybackup', '--no-parse-wal', $tar_backup_path ], + [ 'pg_verifybackup', $tar_backup_path ], $scenario->{'fails_like'}, "corrupt backup fails verification: $name"); diff --git a/src/bin/pg_verifybackup/t/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl index babc4f0a86b8..b07f80719b04 100644 --- a/src/bin/pg_verifybackup/t/007_wal.pl +++ b/src/bin/pg_verifybackup/t/007_wal.pl @@ -42,10 +42,10 @@ command_ok( [ 'pg_verifybackup', - '--wal-directory' => $relocated_pg_wal, + '--wal-path' => $relocated_pg_wal, $backup_path ], - '--wal-directory can be used to specify WAL directory'); + '--wal-path can be used to specify WAL directory'); # Move directory back to original location. rename($relocated_pg_wal, $original_pg_wal) || die "rename pg_wal back: $!"; diff --git a/src/bin/pg_verifybackup/t/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl index bc3d6b352ad5..0cfe1f9532ca 100644 --- a/src/bin/pg_verifybackup/t/008_untar.pl +++ b/src/bin/pg_verifybackup/t/008_untar.pl @@ -123,8 +123,7 @@ # Verify tar backup. $primary->command_ok( [ - 'pg_verifybackup', '--no-parse-wal', - '--exit-on-error', $backup_path, + 'pg_verifybackup', '--exit-on-error', $backup_path, ], "verify backup, compression $method"); diff --git a/src/bin/pg_verifybackup/t/010_client_untar.pl b/src/bin/pg_verifybackup/t/010_client_untar.pl index b62faeb5acfa..76269a73673e 100644 --- a/src/bin/pg_verifybackup/t/010_client_untar.pl +++ b/src/bin/pg_verifybackup/t/010_client_untar.pl @@ -137,8 +137,7 @@ # Verify tar backup. $primary->command_ok( [ - 'pg_verifybackup', '--no-parse-wal', - '--exit-on-error', $backup_path, + 'pg_verifybackup', '--exit-on-error', $backup_path, ], "verify backup, compression $method"); diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile index 4c1ee649501f..b234613eb507 100644 --- a/src/bin/pg_waldump/Makefile +++ b/src/bin/pg_waldump/Makefile @@ -3,6 +3,9 @@ PGFILEDESC = "pg_waldump - decode and display WAL" PGAPPICON=win32 +# make these available to TAP test scripts +export TAR + subdir = src/bin/pg_waldump top_builddir = ../../.. include $(top_builddir)/src/Makefile.global @@ -12,11 +15,13 @@ OBJS = \ $(WIN32RES) \ compat.o \ pg_waldump.o \ + astreamer_waldump.o \ rmgrdesc.o \ xlogreader.o \ xlogstats.o -override CPPFLAGS := -DFRONTEND $(CPPFLAGS) +override CPPFLAGS := -DFRONTEND -I$(libpq_srcdir) $(CPPFLAGS) +LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils RMGRDESCSOURCES = $(sort $(notdir $(wildcard $(top_srcdir)/src/backend/access/rmgrdesc/*desc*.c))) RMGRDESCOBJS = $(patsubst %.c,%.o,$(RMGRDESCSOURCES)) diff --git a/src/bin/pg_waldump/astreamer_waldump.c b/src/bin/pg_waldump/astreamer_waldump.c new file mode 100644 index 000000000000..40876c77f6c5 --- /dev/null +++ b/src/bin/pg_waldump/astreamer_waldump.c @@ -0,0 +1,538 @@ +/*------------------------------------------------------------------------- + * + * astreamer_waldump.c + * A generic facility for reading WAL data from tar archives via archive + * streamer. + * + * Portions Copyright (c) 2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/bin/pg_waldump/astreamer_waldump.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include + +#include "access/xlog_internal.h" +#include "access/xlogdefs.h" +#include "common/file_perm.h" +#include "common/logging.h" +#include "pg_waldump.h" + +/* + * How many bytes should we try to read from a file at once? + */ +#define READ_CHUNK_SIZE (128 * 1024) + +/* + * When nextSegNo is 0, read from any available WAL file. + */ +#define READ_ANY_WAL(mystreamer) ((mystreamer)->nextSegNo == 0) + +typedef struct astreamer_waldump +{ + /* These fields don't change once initialized. */ + astreamer base; + XLogSegNo startSegNo; + XLogSegNo endSegNo; + XLogDumpPrivate *privateInfo; + + /* These fields change with archive member. */ + bool skipThisSeg; + bool writeThisSeg; + FILE *segFp; + XLogSegNo nextSegNo; /* Next expected segment to stream */ +} astreamer_waldump; + +static void astreamer_waldump_content(astreamer *streamer, + astreamer_member *member, + const char *data, int len, + astreamer_archive_context context); +static void astreamer_waldump_finalize(astreamer *streamer); +static void astreamer_waldump_free(astreamer *streamer); + +static bool member_is_relevant_wal(astreamer_waldump *mystreamer, + astreamer_member *member, + TimeLineID startTimeLineID, + char **curFname, + XLogSegNo *curSegNo); +static FILE *member_prepare_tmp_write(XLogSegNo curSegNo, + const char *fname); +static XLogSegNo member_next_segno(XLogSegNo curSegNo, + TimeLineID timeline); + +static const astreamer_ops astreamer_waldump_ops = { + .content = astreamer_waldump_content, + .finalize = astreamer_waldump_finalize, + .free = astreamer_waldump_free +}; + +/* + * Copies WAL data from astreamer to readBuff; if unavailable, fetches more + * from the tar archive via astreamer. + */ +int +astreamer_wal_read(char *readBuff, XLogRecPtr targetPagePtr, Size count, + XLogDumpPrivate *privateInfo) +{ + char *p = readBuff; + Size nbytes = count; + XLogRecPtr recptr = targetPagePtr; + volatile StringInfo astreamer_buf = privateInfo->archive_streamer_buf; + + while (nbytes > 0) + { + char *buf = astreamer_buf->data; + int len = astreamer_buf->len; + + /* WAL record range that the buffer contains */ + XLogRecPtr endPtr = privateInfo->archive_streamer_read_ptr; + XLogRecPtr startPtr = (endPtr > len) ? endPtr - len : 0; + + /* + * pg_waldump never ask the same WAL bytes more than once, so if we're + * now being asked for data beyond the end of what we've already read, + * that means none of the data we currently have in the buffer will + * ever be consulted again. So, we can discard the existing buffer + * contents and start over. + */ + if (recptr >= endPtr) + { + len = 0; + + /* Discard the buffered data */ + resetStringInfo(astreamer_buf); + } + + if (len > 0 && recptr > startPtr) + { + int skipBytes = 0; + + /* + * The required offset is not at the start of the archive streamer + * buffer, so skip bytes until reaching the desired offset of the + * target page. + */ + skipBytes = recptr - startPtr; + + buf += skipBytes; + len -= skipBytes; + } + + if (len > 0) + { + int readBytes = len >= nbytes ? nbytes : len; + + /* + * Ensure we are reading the correct page, unless we've received + * an invalid record pointer. In that specific case, it's + * acceptable to read any page. + */ + Assert(XLogRecPtrIsInvalid(recptr) || + (recptr >= startPtr && recptr < endPtr)); + + memcpy(p, buf, readBytes); + + /* Update state for read */ + nbytes -= readBytes; + p += readBytes; + recptr += readBytes; + } + else + { + /* Fetch more data */ + if (astreamer_archive_read(privateInfo) == 0) + { + char fname[MAXFNAMELEN]; + XLogSegNo segno; + + XLByteToSeg(targetPagePtr, segno, WalSegSz); + XLogFileName(fname, privateInfo->timeline, segno, WalSegSz); + + pg_fatal("could not find file \"%s\" in \"%s\" archive", + fname, privateInfo->archive_name); + } + } + } + + /* + * Should have either have successfully read all the requested bytes or + * reported a failure before this point. + */ + Assert(nbytes == 0); + + return count; +} + +/* + * Reads the archive and passes it to the archive streamer for decompression. + */ +int +astreamer_archive_read(XLogDumpPrivate *privateInfo) +{ + int rc; + char *buffer; + + buffer = pg_malloc(READ_CHUNK_SIZE * sizeof(uint8)); + + /* Read more data from the tar file */ + rc = read(privateInfo->archive_fd, buffer, READ_CHUNK_SIZE); + if (rc < 0) + pg_fatal("could not read file \"%s\": %m", + privateInfo->archive_name); + + /* + * Decrypt (if required), and then parse the previously read contents of + * the tar file. + */ + if (rc > 0) + astreamer_content(privateInfo->archive_streamer, NULL, + buffer, rc, ASTREAMER_UNKNOWN); + pg_free(buffer); + + return rc; +} + +/* + * Create an astreamer that can read WAL from tar file. + */ +astreamer * +astreamer_waldump_new(XLogRecPtr startptr, XLogRecPtr endPtr, + XLogDumpPrivate *privateInfo) +{ + astreamer_waldump *streamer; + + streamer = palloc0(sizeof(astreamer_waldump)); + *((const astreamer_ops **) &streamer->base.bbs_ops) = + &astreamer_waldump_ops; + + initStringInfo(&streamer->base.bbs_buffer); + + if (XLogRecPtrIsInvalid(startptr)) + streamer->startSegNo = 0; + else + XLByteToSeg(startptr, streamer->startSegNo, WalSegSz); + + if (XLogRecPtrIsInvalid(endPtr)) + streamer->endSegNo = UINT64_MAX; + else + XLByteToSeg(endPtr, streamer->endSegNo, WalSegSz); + + streamer->nextSegNo = streamer->startSegNo; + streamer->privateInfo = privateInfo; + + return &streamer->base; +} + +/* + * Main entry point of the archive streamer for reading WAL from a tar file. + */ +static void +astreamer_waldump_content(astreamer *streamer, astreamer_member *member, + const char *data, int len, + astreamer_archive_context context) +{ + astreamer_waldump *mystreamer = (astreamer_waldump *) streamer; + XLogDumpPrivate *privateInfo = mystreamer->privateInfo; + + Assert(context != ASTREAMER_UNKNOWN); + + switch (context) + { + case ASTREAMER_MEMBER_HEADER: + { + char *fname; + + pg_log_debug("pg_waldump: reading \"%s\"", member->pathname); + + mystreamer->skipThisSeg = false; + mystreamer->writeThisSeg = false; + + if (!member_is_relevant_wal(mystreamer, member, + privateInfo->timeline, + &fname, &privateInfo->curSegNo)) + { + mystreamer->skipThisSeg = true; + break; + } + + /* + * Further checks are skipped if any WAL file can be read. + * This typically occurs during initial verification. + */ + if (READ_ANY_WAL(mystreamer)) + break; + + /* + * When WAL segments are not archived sequentially, it becomes + * necessary to write out (or preserve) segments that might be + * required at a later point. + */ + if (mystreamer->nextSegNo != privateInfo->curSegNo) + { + mystreamer->writeThisSeg = true; + mystreamer->segFp = + member_prepare_tmp_write(privateInfo->curSegNo, fname); + break; + } + + /* + * If the buffer contains data, the next WAL record must + * logically follow it. Otherwise, this file isn't the one we + * need, and we must export it. + */ + else if (privateInfo->archive_streamer_buf->len != 0) + { + XLogRecPtr recPtr; + + XLogSegNoOffsetToRecPtr(privateInfo->curSegNo, 0, WalSegSz, + recPtr); + + if (privateInfo->archive_streamer_read_ptr != recPtr) + { + mystreamer->writeThisSeg = true; + mystreamer->segFp = + member_prepare_tmp_write(privateInfo->curSegNo, fname); + + /* Update the next expected segment number after this */ + mystreamer->nextSegNo = + member_next_segno(privateInfo->curSegNo + 1, + privateInfo->timeline); + break; + } + } + + Assert(!mystreamer->skipThisSeg); + Assert(!mystreamer->writeThisSeg); + + /* + * We are now streaming segment containt. + * + * We need to track the reading of WAL segment records using a + * pointer that's typically incremented by the length of the + * data read. However, we sometimes export the WAL file to + * temporary storage, allowing the decoding routine to read + * directly from there. This makes continuous pointer + * incrementing challenging, as file reads can occur from any + * offset, leading to potential errors. Therefore, we now + * reset the pointer when reading from a file for streaming. + */ + XLogSegNoOffsetToRecPtr(privateInfo->curSegNo, 0, WalSegSz, + privateInfo->archive_streamer_read_ptr); + + /* Update the next expected segment number */ + mystreamer->nextSegNo = + member_next_segno(privateInfo->curSegNo, + privateInfo->timeline); + } + break; + + case ASTREAMER_MEMBER_CONTENTS: + /* Skip this segment */ + if (mystreamer->skipThisSeg) + break; + + /* Or, write contents to file */ + if (mystreamer->writeThisSeg) + { + Assert(mystreamer->segFp != NULL); + + errno = 0; + if (len > 0 && fwrite(data, len, 1, mystreamer->segFp) != 1) + { + char *fname; + int pathlen = strlen(member->pathname); + + Assert(pathlen >= XLOG_FNAME_LEN); + + fname = member->pathname + (pathlen - XLOG_FNAME_LEN); + + /* + * If write didn't set errno, assume problem is no disk + * space + */ + if (errno == 0) + errno = ENOSPC; + pg_fatal("could not write to file \"%s\": %m", + get_tmp_wal_file_path(fname)); + } + break; + } + + /* Or, copy contents to buffer */ + privateInfo->archive_streamer_read_ptr += len; + astreamer_buffer_bytes(streamer, &data, &len, len); + break; + + case ASTREAMER_MEMBER_TRAILER: + if (mystreamer->segFp != NULL) + { + fclose(mystreamer->segFp); + mystreamer->segFp = NULL; + } + privateInfo->curSegNo = 0; + break; + + case ASTREAMER_ARCHIVE_TRAILER: + break; + + default: + /* Shouldn't happen. */ + pg_fatal("unexpected state while parsing tar file"); + } +} + +/* + * End-of-stream processing for a astreamer_waldump stream. + */ +static void +astreamer_waldump_finalize(astreamer *streamer) +{ + Assert(streamer->bbs_next == NULL); +} + +/* + * Free memory associated with a astreamer_waldump stream. + */ +static void +astreamer_waldump_free(astreamer *streamer) +{ + astreamer_waldump *mystreamer; + + Assert(streamer->bbs_next == NULL); + + mystreamer = (astreamer_waldump *) streamer; + if (mystreamer->segFp != NULL) + fclose(mystreamer->segFp); + + pfree(streamer->bbs_buffer.data); + pfree(streamer); +} + +/* + * Returns true if the archive member name matches the WAL naming format and + * the corresponding WAL segment falls within the WAL decoding target range; + * otherwise, returns false. + */ +static bool +member_is_relevant_wal(astreamer_waldump *mystreamer, astreamer_member *member, + TimeLineID startTimeLineID, char **curFname, + XLogSegNo *curSegNo) +{ + int pathlen; + XLogSegNo segNo; + TimeLineID timeline; + char *fname; + + /* We are only interested in normal files. */ + if (member->is_directory || member->is_link) + return false; + + pathlen = strlen(member->pathname); + if (pathlen < XLOG_FNAME_LEN) + return false; + + /* WAL file could be with full path */ + fname = member->pathname + (pathlen - XLOG_FNAME_LEN); + if (!IsXLogFileName(fname)) + return false; + + /* Parse position from file */ + XLogFromFileName(fname, &timeline, &segNo, WalSegSz); + + /* No further checks are needed if any file ask to read */ + if (!READ_ANY_WAL(mystreamer)) + { + /* Ignore if the timeline is different */ + if (startTimeLineID != timeline) + return false; + + /* Skip if the current segment is not the desired one */ + if (mystreamer->startSegNo > segNo || mystreamer->endSegNo < segNo) + return false; + } + + *curFname = fname; + *curSegNo = segNo; + + return true; +} + +/* + * Create an empty placeholder file and return its handle. The file is also + * added to an exported list for future management, e.g. access, deletion, and + * existence checks. + */ +static FILE * +member_prepare_tmp_write(XLogSegNo curSegNo, const char *fname) +{ + FILE *file; + char *fpath = get_tmp_wal_file_path(fname); + + /* Create an empty placeholder */ + file = fopen(fpath, PG_BINARY_W); + if (file == NULL) + pg_fatal("could not create file \"%s\": %m", fpath); + +#ifndef WIN32 + if (chmod(fpath, pg_file_create_mode)) + pg_fatal("could not set permissions on file \"%s\": %m", + fpath); +#endif + + pg_log_info("temporarily exporting file \"%s\"", fpath); + + /* Record this segment's export */ + simple_string_list_append(&TmpWalSegList, fname); + pfree(fpath); + + return file; +} + +/* + * Get next WAL segment that needs to be retrieved from the archive. + * + * The function checks for the presence of a previously read and extracted WAL + * segment in the temporary storage. If a temporary file is found for that + * segment, it indicates the segment has already been successfully retrieved + * from the archive. In this case, the function increments the segment number + * and repeats the check. This process continues until a segment that has not + * yet been retrieved is found, at which point the function returns its number. + */ +static XLogSegNo +member_next_segno(XLogSegNo curSegNo, TimeLineID timeline) +{ + XLogSegNo nextSegNo = curSegNo + 1; + bool exists; + + /* + * If we find a file that was previously written to the temporary space, + * it indicates that the corresponding WAL segment request has already + * been fulfilled. In that case, we increment the nextSegNo counter and + * check again whether that segment number again. if found above steps + * will be return if not then we return that segment number which would be + * needed from the archive. + */ + do + { + char fname[MAXFNAMELEN]; + + XLogFileName(fname, timeline, nextSegNo, WalSegSz); + + /* + * If the WAL segment has already been exported, increment the counter + * and check for the next segment. + */ + exists = false; + if (simple_string_list_member(&TmpWalSegList, fname)) + { + nextSegNo += 1; + exists = true; + } + } while (exists); + + return nextSegNo; +} diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build index 937e0d688414..2a0300dc3391 100644 --- a/src/bin/pg_waldump/meson.build +++ b/src/bin/pg_waldump/meson.build @@ -3,6 +3,7 @@ pg_waldump_sources = files( 'compat.c', 'pg_waldump.c', + 'astreamer_waldump.c', 'rmgrdesc.c', ) @@ -18,7 +19,7 @@ endif pg_waldump = executable('pg_waldump', pg_waldump_sources, - dependencies: [frontend_code, lz4, zstd], + dependencies: [frontend_code, lz4, zstd, libpq], c_args: ['-DFRONTEND'], # needed for xlogreader et al kwargs: default_bin_args, ) @@ -29,6 +30,7 @@ tests += { 'sd': meson.current_source_dir(), 'bd': meson.current_build_dir(), 'tap': { + 'env': {'TAR': tar.found() ? tar.full_path() : ''}, 'tests': [ 't/001_basic.pl', 't/002_save_fullpage.pl', diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c index 13d3ec2f5be3..615227b691c0 100644 --- a/src/bin/pg_waldump/pg_waldump.c +++ b/src/bin/pg_waldump/pg_waldump.c @@ -29,6 +29,7 @@ #include "common/logging.h" #include "common/relpath.h" #include "getopt_long.h" +#include "pg_waldump.h" #include "rmgrdesc.h" #include "storage/bufpage.h" @@ -39,18 +40,14 @@ static const char *progname; -static int WalSegSz; +int WalSegSz = DEFAULT_XLOG_SEG_SIZE; static volatile sig_atomic_t time_to_stop = false; -static const RelFileLocator emptyRelFileLocator = {0, 0, 0}; +/* Temporary exported WAL file directory and the list */ +char *TmpWalSegDir = NULL; +SimpleStringList TmpWalSegList = {NULL, NULL}; -typedef struct XLogDumpPrivate -{ - TimeLineID timeline; - XLogRecPtr startptr; - XLogRecPtr endptr; - bool endptr_reached; -} XLogDumpPrivate; +static const RelFileLocator emptyRelFileLocator = {0, 0, 0}; typedef struct XLogDumpConfig { @@ -333,6 +330,207 @@ identify_target_directory(char *directory, char *fname) return NULL; /* not reached */ } +/* + * Returns true if the given file is a tar archive and outputs its compression + * algorithm. + */ +static bool +is_tar_file(const char *fname, pg_compress_algorithm *compression) +{ + int fname_len = strlen(fname); + pg_compress_algorithm compress_algo; + + /* Now, check the compression type of the tar */ + if (fname_len > 4 && + strcmp(fname + fname_len - 4, ".tar") == 0) + compress_algo = PG_COMPRESSION_NONE; + else if (fname_len > 4 && + strcmp(fname + fname_len - 4, ".tgz") == 0) + compress_algo = PG_COMPRESSION_GZIP; + else if (fname_len > 7 && + strcmp(fname + fname_len - 7, ".tar.gz") == 0) + compress_algo = PG_COMPRESSION_GZIP; + else if (fname_len > 8 && + strcmp(fname + fname_len - 8, ".tar.lz4") == 0) + compress_algo = PG_COMPRESSION_LZ4; + else if (fname_len > 8 && + strcmp(fname + fname_len - 8, ".tar.zst") == 0) + compress_algo = PG_COMPRESSION_ZSTD; + else + return false; + + *compression = compress_algo; + + return true; +} + +/* + * Set up a temporary directory to temporarily store WAL segments. + */ +static void +setup_tmp_walseg_dir(const char *waldir) +{ + /* + * Use the directory specified by the TEMDIR environment variable. If it's + * not set, use the provided WAL directory. + */ + TmpWalSegDir = getenv("TMPDIR") ? + pg_strdup(getenv("TMPDIR")) : pg_strdup(waldir); + canonicalize_path(TmpWalSegDir); +} + +/* + * Removes the temporarily store WAL segments, if any at exiting. + */ +static void +remove_tmp_walseg_dir_atexit(void) +{ + SimpleStringListCell *cell; + + /* Clear out any existing temporary files */ + for (cell = TmpWalSegList.head; cell; cell = cell->next) + { + char *fpath = get_tmp_wal_file_path(cell->val); + + if (unlink(fpath) == 0) + pg_log_info("removed file \"%s\"", fpath); + pfree(fpath); + } +} + + +/* + * Initializes the tar archive reader and a temporary directory for WAL files. + */ +static void +init_tar_archive_reader(XLogDumpPrivate *private, const char *waldir, + XLogRecPtr startptr, XLogRecPtr endptr, + pg_compress_algorithm compression) +{ + int fd; + astreamer *streamer; + + /* Open tar archive and store its file descriptor */ + fd = open_file_in_directory(waldir, private->archive_name); + + if (fd < 0) + pg_fatal("could not open file \"%s\"", private->archive_name); + + private->archive_fd = fd; + + /* + * Create an appropriate chain of archive streamers for reading the given + * tar archive. + */ + streamer = astreamer_waldump_new(startptr, endptr, private); + + /* + * Final extracted WAL data will reside in this streamer. However, since + * it sits at the bottom of the stack and isn't designed to propagate data + * upward, we need to hold a pointer to its data buffer in order to copy. + */ + private->archive_streamer_buf = &streamer->bbs_buffer; + + /* Before that we must parse the tar archive. */ + streamer = astreamer_tar_parser_new(streamer); + + /* Before that we must decompress, if archive is compressed. */ + if (compression == PG_COMPRESSION_GZIP) + streamer = astreamer_gzip_decompressor_new(streamer); + else if (compression == PG_COMPRESSION_LZ4) + streamer = astreamer_lz4_decompressor_new(streamer); + else if (compression == PG_COMPRESSION_ZSTD) + streamer = astreamer_zstd_decompressor_new(streamer); + + private->archive_streamer = streamer; + private->curSegNo = 0; +} + +/* + * Release the archive streamer chain and close the archive file. + */ +static void +free_tar_archive_reader(XLogDumpPrivate *private) +{ + /* + * NB: Normally, astreamer_finalize() is called before astreamer_free() to + * flush any remaining buffered data or to ensure the end of the tar + * archive is reached. However, when decoding a WAL file, once we hit the + * end LSN, any remaining WAL data in the buffer or the tar archive's + * unreached end can be safely ignored. + */ + astreamer_free(private->archive_streamer); + + /* Close the file. */ + if (close(private->archive_fd) != 0) + pg_log_error("could not close file \"%s\": %m", + private->archive_name); +} + +/* + * Reads a WAL page from the archive and verifies WAL segment size. + */ +static void +verify_tar_archive(XLogDumpPrivate *private, const char *waldir, + pg_compress_algorithm compression) +{ + PGAlignedXLogBlock buf; + int r; + + /* Initialize the reader to stream WAL data from a tar file */ + init_tar_archive_reader(private, waldir, InvalidXLogRecPtr, + InvalidXLogRecPtr, compression); + + /* Read a wal page */ + r = astreamer_wal_read(buf.data, InvalidXLogRecPtr, XLOG_BLCKSZ, private); + + /* Set WalSegSz if WAL data is successfully read */ + if (r == XLOG_BLCKSZ) + { + XLogLongPageHeader longhdr = (XLogLongPageHeader) buf.data; + + WalSegSz = longhdr->xlp_seg_size; + + if (!IsValidWalSegSize(WalSegSz)) + { + pg_log_error(ngettext("invalid WAL segment size in WAL file \"%s\" (%d byte)", + "invalid WAL segment size in WAL file \"%s\" (%d bytes)", + WalSegSz), + private->archive_name, WalSegSz); + pg_log_error_detail("The WAL segment size must be a power of two between 1 MB and 1 GB."); + exit(1); + } + } + else + pg_fatal("could not read WAL data from \"%s\" archive: read %d of %d", + private->archive_name, r, XLOG_BLCKSZ); + + free_tar_archive_reader(private); +} + +/* Returns the size in bytes of the data to be read. */ +static inline int +required_read_len(XLogDumpPrivate *private, XLogRecPtr targetPagePtr, + int reqLen) +{ + int count = XLOG_BLCKSZ; + + if (private->endptr != InvalidXLogRecPtr) + { + if (targetPagePtr + XLOG_BLCKSZ <= private->endptr) + count = XLOG_BLCKSZ; + else if (targetPagePtr + reqLen <= private->endptr) + count = private->endptr - targetPagePtr; + else + { + private->endptr_reached = true; + return -1; + } + } + + return count; +} + /* pg_waldump's XLogReaderRoutine->segment_open callback */ static void WALDumpOpenSegment(XLogReaderState *state, XLogSegNo nextSegNo, @@ -390,21 +588,11 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen, XLogRecPtr targetPtr, char *readBuff) { XLogDumpPrivate *private = state->private_data; - int count = XLOG_BLCKSZ; + int count = required_read_len(private, targetPagePtr, reqLen); WALReadError errinfo; - if (private->endptr != InvalidXLogRecPtr) - { - if (targetPagePtr + XLOG_BLCKSZ <= private->endptr) - count = XLOG_BLCKSZ; - else if (targetPagePtr + reqLen <= private->endptr) - count = private->endptr - targetPagePtr; - else - { - private->endptr_reached = true; - return -1; - } - } + if (private->endptr_reached) + return -1; if (!WALRead(state, readBuff, targetPagePtr, count, private->timeline, &errinfo)) @@ -430,6 +618,102 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen, return count; } +/* + * pg_waldump's XLogReaderRoutine->segment_open callback to support dumping WAL + * files from tar archives. + */ +static void +TarWALDumpOpenSegment(XLogReaderState *state, XLogSegNo nextSegNo, + TimeLineID *tli_p) +{ + /* No action needed */ +} + +/* + * pg_waldump's XLogReaderRoutine->segment_close callback. + */ +static void +TarWALDumpCloseSegment(XLogReaderState *state) +{ + /* No action needed */ +} + +/* + * pg_waldump's XLogReaderRoutine->page_read callback to support dumping WAL + * files from tar archives. + */ +static int +TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen, + XLogRecPtr targetPtr, char *readBuff) +{ + XLogDumpPrivate *private = state->private_data; + int count = required_read_len(private, targetPagePtr, reqLen); + XLogSegNo nextSegNo; + + if (private->endptr_reached) + return -1; + + /* + * If the target page is in a different segment, first check for the WAL + * segment's physical existence in the temporary directory. + */ + nextSegNo = state->seg.ws_segno; + if (!XLByteInSeg(targetPagePtr, nextSegNo, WalSegSz)) + { + char fname[MAXFNAMELEN]; + char *fpath; + + if (state->seg.ws_file >= 0) + { + close(state->seg.ws_file); + state->seg.ws_file = -1; + + /* Remove this file, as it is no longer needed. */ + XLogFileName(fname, state->seg.ws_tli, nextSegNo, WalSegSz); + fpath = get_tmp_wal_file_path(fname); + pg_log_info("removing file \"%s\"", fpath); + unlink(fpath); + pfree(fpath); + } + + XLByteToSeg(targetPagePtr, nextSegNo, WalSegSz); + state->seg.ws_tli = private->timeline; + state->seg.ws_segno = nextSegNo; + + /* + * If the next segment exists, open it and continue reading from there + */ + XLogFileName(fname, private->timeline, nextSegNo, WalSegSz); + if (simple_string_list_member(&TmpWalSegList, fname)) + { + fpath = get_tmp_wal_file_path(fname); + state->seg.ws_file = open(fpath, O_RDONLY | PG_BINARY, 0); + + if (state->seg.ws_file < 0) + pg_fatal("could not open file \"%s\": %m", fpath); + pfree(fpath); + } + } + + /* Continue reading from the open WAL segment, if any */ + if (state->seg.ws_file >= 0) + { + /* + * To prevent a race condition where the archive streamer is still + * exporting a file that we are trying to read, we invoke the streamer + * to ensure enough data is available. + */ + if (private->curSegNo == state->seg.ws_segno) + astreamer_archive_read(private); + + return WALDumpReadPage(state, targetPagePtr, reqLen, targetPtr, + readBuff); + } + + /* Otherwise, read the WAL page from the archive streamer */ + return astreamer_wal_read(readBuff, targetPagePtr, count, private); +} + /* * Boolean to return whether the given WAL record matches a specific relation * and optionally block. @@ -767,8 +1051,8 @@ usage(void) printf(_(" -F, --fork=FORK only show records that modify blocks in fork FORK;\n" " valid names are main, fsm, vm, init\n")); printf(_(" -n, --limit=N number of records to display\n")); - printf(_(" -p, --path=PATH directory in which to find WAL segment files or a\n" - " directory with a ./pg_wal that contains such files\n" + printf(_(" -p, --path=PATH tar archive or a directory in which to find WAL segment files or\n" + " a directory with a ./pg_wal that contains such files\n" " (default: current directory, ./pg_wal, $PGDATA/pg_wal)\n")); printf(_(" -q, --quiet do not print any output, except for errors\n")); printf(_(" -r, --rmgr=RMGR only show records generated by resource manager RMGR;\n" @@ -800,7 +1084,10 @@ main(int argc, char **argv) XLogRecord *record; XLogRecPtr first_record; char *waldir = NULL; + char *walpath = NULL; char *errormsg; + bool is_tar = false; + pg_compress_algorithm compression; static struct option long_options[] = { {"bkp-details", no_argument, NULL, 'b'}, @@ -932,7 +1219,7 @@ main(int argc, char **argv) } break; case 'p': - waldir = pg_strdup(optarg); + walpath = pg_strdup(optarg); break; case 'q': config.quiet = true; @@ -1096,10 +1383,20 @@ main(int argc, char **argv) goto bad_argument; } - if (waldir != NULL) + if (walpath != NULL) { + /* validate path points to tar archive */ + if (is_tar_file(walpath, &compression)) + { + char *fname = NULL; + + split_path(walpath, &waldir, &fname); + + private.archive_name = fname; + is_tar = true; + } /* validate path points to directory */ - if (!verify_directory(waldir)) + else if (!verify_directory(walpath)) { pg_log_error("could not open directory \"%s\": %m", waldir); goto bad_argument; @@ -1117,46 +1414,35 @@ main(int argc, char **argv) int fd; XLogSegNo segno; + /* + * If a tar archive is passed using the --path option, all other + * arguments become unnecessary. + */ + if (is_tar) + { + pg_log_error("unnecessary command-line arguments specified with tar archive (first is \"%s\")", + argv[optind]); + goto bad_argument; + } + split_path(argv[optind], &directory, &fname); - if (waldir == NULL && directory != NULL) + if (walpath == NULL && directory != NULL) { - waldir = directory; + walpath = directory; - if (!verify_directory(waldir)) + if (!verify_directory(walpath)) pg_fatal("could not open directory \"%s\": %m", waldir); } - waldir = identify_target_directory(waldir, fname); - fd = open_file_in_directory(waldir, fname); - if (fd < 0) - pg_fatal("could not open file \"%s\"", fname); - close(fd); - - /* parse position from file */ - XLogFromFileName(fname, &private.timeline, &segno, WalSegSz); - - if (XLogRecPtrIsInvalid(private.startptr)) - XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr); - else if (!XLByteInSeg(private.startptr, segno, WalSegSz)) + if (fname != NULL && is_tar_file(fname, &compression)) { - pg_log_error("start WAL location %X/%08X is not inside file \"%s\"", - LSN_FORMAT_ARGS(private.startptr), - fname); - goto bad_argument; + private.archive_name = fname; + is_tar = true; } - - /* no second file specified, set end position */ - if (!(optind + 1 < argc) && XLogRecPtrIsInvalid(private.endptr)) - XLogSegNoOffsetToRecPtr(segno + 1, 0, WalSegSz, private.endptr); - - /* parse ENDSEG if passed */ - if (optind + 1 < argc) + else { - XLogSegNo endsegno; - - /* ignore directory, already have that */ - split_path(argv[optind + 1], &directory, &fname); + waldir = identify_target_directory(walpath, fname); fd = open_file_in_directory(waldir, fname); if (fd < 0) @@ -1164,32 +1450,70 @@ main(int argc, char **argv) close(fd); /* parse position from file */ - XLogFromFileName(fname, &private.timeline, &endsegno, WalSegSz); + XLogFromFileName(fname, &private.timeline, &segno, WalSegSz); - if (endsegno < segno) - pg_fatal("ENDSEG %s is before STARTSEG %s", - argv[optind + 1], argv[optind]); + if (XLogRecPtrIsInvalid(private.startptr)) + XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr); + else if (!XLByteInSeg(private.startptr, segno, WalSegSz)) + { + pg_log_error("start WAL location %X/%08X is not inside file \"%s\"", + LSN_FORMAT_ARGS(private.startptr), + fname); + goto bad_argument; + } - if (XLogRecPtrIsInvalid(private.endptr)) - XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz, - private.endptr); + /* no second file specified, set end position */ + if (!(optind + 1 < argc) && XLogRecPtrIsInvalid(private.endptr)) + XLogSegNoOffsetToRecPtr(segno + 1, 0, WalSegSz, private.endptr); - /* set segno to endsegno for check of --end */ - segno = endsegno; - } + /* parse ENDSEG if passed */ + if (optind + 1 < argc) + { + XLogSegNo endsegno; + /* ignore directory, already have that */ + split_path(argv[optind + 1], &directory, &fname); - if (!XLByteInSeg(private.endptr, segno, WalSegSz) && - private.endptr != (segno + 1) * WalSegSz) - { - pg_log_error("end WAL location %X/%08X is not inside file \"%s\"", - LSN_FORMAT_ARGS(private.endptr), - argv[argc - 1]); - goto bad_argument; + fd = open_file_in_directory(waldir, fname); + if (fd < 0) + pg_fatal("could not open file \"%s\"", fname); + close(fd); + + /* parse position from file */ + XLogFromFileName(fname, &private.timeline, &endsegno, WalSegSz); + + if (endsegno < segno) + pg_fatal("ENDSEG %s is before STARTSEG %s", + argv[optind + 1], argv[optind]); + + if (XLogRecPtrIsInvalid(private.endptr)) + XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz, + private.endptr); + + /* set segno to endsegno for check of --end */ + segno = endsegno; + } + + + if (!XLByteInSeg(private.endptr, segno, WalSegSz) && + private.endptr != (segno + 1) * WalSegSz) + { + pg_log_error("end WAL location %X/%08X is not inside file \"%s\"", + LSN_FORMAT_ARGS(private.endptr), + argv[argc - 1]); + goto bad_argument; + } } } - else - waldir = identify_target_directory(waldir, NULL); + else if (!is_tar) + waldir = identify_target_directory(walpath, NULL); + + /* Verify that the archive contains valid WAL files */ + if (is_tar) + { + waldir = waldir ? pg_strdup(waldir) : pg_strdup("."); + verify_tar_archive(&private, waldir, compression); + } /* we don't know what to print */ if (XLogRecPtrIsInvalid(private.startptr)) @@ -1201,12 +1525,38 @@ main(int argc, char **argv) /* done with argument parsing, do the actual work */ /* we have everything we need, start reading */ - xlogreader_state = - XLogReaderAllocate(WalSegSz, waldir, - XL_ROUTINE(.page_read = WALDumpReadPage, - .segment_open = WALDumpOpenSegment, - .segment_close = WALDumpCloseSegment), - &private); + if (is_tar) + { + /* Set up for reading tar file */ + init_tar_archive_reader(&private, waldir, private.startptr, + private.endptr, compression); + + /* + * Setup temporary directory to store WAL segments and set up an exit + * callback to remove it upon completion. + */ + setup_tmp_walseg_dir(waldir); + atexit(remove_tmp_walseg_dir_atexit); + + /* Routine to decode WAL files in tar archive */ + xlogreader_state = + XLogReaderAllocate(WalSegSz, waldir, + XL_ROUTINE(.page_read = TarWALDumpReadPage, + .segment_open = TarWALDumpOpenSegment, + .segment_close = TarWALDumpCloseSegment), + &private); + } + else + { + /* Routine to decode WAL files */ + xlogreader_state = + XLogReaderAllocate(WalSegSz, waldir, + XL_ROUTINE(.page_read = WALDumpReadPage, + .segment_open = WALDumpOpenSegment, + .segment_close = WALDumpCloseSegment), + &private); + } + if (!xlogreader_state) pg_fatal("out of memory while allocating a WAL reading processor"); @@ -1315,6 +1665,9 @@ main(int argc, char **argv) XLogReaderFree(xlogreader_state); + if (is_tar) + free_tar_archive_reader(&private); + return EXIT_SUCCESS; bad_argument: diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h new file mode 100644 index 000000000000..1a1cf35e6f38 --- /dev/null +++ b/src/bin/pg_waldump/pg_waldump.h @@ -0,0 +1,67 @@ +/*------------------------------------------------------------------------- + * + * pg_waldump.h - decode and display WAL + * + * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/bin/pg_waldump/pg_waldump.h + *------------------------------------------------------------------------- + */ +#ifndef PG_WALDUMP_H +#define PG_WALDUMP_H + +#include "access/xlogdefs.h" +#include "fe_utils/astreamer.h" +#include "fe_utils/simple_list.h" +#include "lib/stringinfo.h" + +#define TEMP_FILE_EXT "waldump.tmp" + +extern int WalSegSz; +extern char *TmpWalSegDir; +extern SimpleStringList TmpWalSegList; + +/* Contains the necessary information to drive WAL decoding */ +typedef struct XLogDumpPrivate +{ + TimeLineID timeline; + XLogRecPtr startptr; + XLogRecPtr endptr; + bool endptr_reached; + + /* Fields required to read WAL from archive */ + char *archive_name; /* Tar archive name */ + int archive_fd; /* File descriptor for the open tar file */ + + astreamer *archive_streamer; + StringInfo archive_streamer_buf; /* Buffer for receiving WAL data */ + XLogRecPtr archive_streamer_read_ptr; /* Populate the buffer with + * records until this record + * pointer */ + XLogSegNo curSegNo; /* Current segment being read */ +} XLogDumpPrivate; + +/* + * Generate the temporary WAL file path. + * + * Note that the caller is responsible to pfree it. + */ +static inline char * +get_tmp_wal_file_path(const char *fname) +{ + char *fpath = (char *) palloc(MAXPGPATH); + + snprintf(fpath, MAXPGPATH, "%s/%s.%s", TmpWalSegDir, fname, + TEMP_FILE_EXT); + + return fpath; +} + +extern astreamer *astreamer_waldump_new(XLogRecPtr startptr, XLogRecPtr endptr, + XLogDumpPrivate *privateInfo); +extern int astreamer_wal_read(char *readBuff, XLogRecPtr startptr, Size count, + XLogDumpPrivate *privateInfo); +extern int astreamer_archive_read(XLogDumpPrivate *privateInfo); + +#endif /* end of PG_WALDUMP_H */ diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl index f26d75e01cfd..d5fa1f6d28dc 100644 --- a/src/bin/pg_waldump/t/001_basic.pl +++ b/src/bin/pg_waldump/t/001_basic.pl @@ -3,9 +3,13 @@ use strict; use warnings FATAL => 'all'; +use Cwd; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; +use List::Util qw(shuffle); + +my $tar = $ENV{TAR}; program_help_ok('pg_waldump'); program_version_ok('pg_waldump'); @@ -198,28 +202,6 @@ ], qr/./, 'runs with start and end segment specified'); -command_fails_like( - [ 'pg_waldump', '--path' => $node->data_dir ], - qr/error: no start WAL location given/, - 'path option requires start location'); -command_like( - [ - 'pg_waldump', - '--path' => $node->data_dir, - '--start' => $start_lsn, - '--end' => $end_lsn, - ], - qr/./, - 'runs with path option and start and end locations'); -command_fails_like( - [ - 'pg_waldump', - '--path' => $node->data_dir, - '--start' => $start_lsn, - ], - qr/error: error in WAL record at/, - 'falling off the end of the WAL results in an error'); - command_like( [ 'pg_waldump', '--quiet', @@ -227,15 +209,6 @@ ], qr/^$/, 'no output with --quiet option'); -command_fails_like( - [ - 'pg_waldump', '--quiet', - '--path' => $node->data_dir, - '--start' => $start_lsn - ], - qr/error: error in WAL record at/, - 'errors are shown with --quiet'); - # Test for: Display a message that we're skipping data if `from` # wasn't a pointer to the start of a record. @@ -266,15 +239,15 @@ sub test_pg_waldump { local $Test::Builder::Level = $Test::Builder::Level + 1; - my @opts = @_; + my ($path, @opts) = @_; my ($stdout, $stderr); my $result = IPC::Run::run [ 'pg_waldump', - '--path' => $node->data_dir, '--start' => $start_lsn, '--end' => $end_lsn, + '--path' => $path, @opts ], '>' => \$stdout, @@ -286,40 +259,139 @@ sub test_pg_waldump return @lines; } -my @lines; - -@lines = test_pg_waldump; -is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines'); - -@lines = test_pg_waldump('--limit' => 6); -is(@lines, 6, 'limit option observed'); - -@lines = test_pg_waldump('--fullpage'); -is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW'); - -@lines = test_pg_waldump('--stats'); -like($lines[0], qr/WAL statistics/, "statistics on stdout"); -is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output'); - -@lines = test_pg_waldump('--stats=record'); -like($lines[0], qr/WAL statistics/, "statistics on stdout"); -is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output'); - -@lines = test_pg_waldump('--rmgr' => 'Btree'); -is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines'); - -@lines = test_pg_waldump('--fork' => 'init'); -is(grep(!/fork init/, @lines), 0, 'only init fork lines'); - -@lines = test_pg_waldump( - '--relation' => "$default_ts_oid/$postgres_db_oid/$rel_t1_oid"); -is(grep(!/rel $default_ts_oid\/$postgres_db_oid\/$rel_t1_oid/, @lines), - 0, 'only lines for selected relation'); - -@lines = test_pg_waldump( - '--relation' => "$default_ts_oid/$postgres_db_oid/$rel_i1a_oid", - '--block' => 1); -is(grep(!/\bblk 1\b/, @lines), 0, 'only lines for selected block'); +# Create a tar archive, sorting the file order +sub generate_archive +{ + my ($archive, $directory, $compression_flags) = @_; + + my @files; + opendir my $dh, $directory or die "opendir: $!"; + while (my $entry = readdir $dh) { + # Skip '.' and '..' + next if $entry eq '.' || $entry eq '..'; + push @files, $entry; + } + closedir $dh; + + @files = shuffle @files; + + # move into the WAL directory before archiving files + my $cwd = getcwd; + chdir($directory) || die "chdir: $!"; + command_ok([$tar, $compression_flags, $archive, @files]); + chdir($cwd) || die "chdir: $!"; +} +my $tmp_dir = PostgreSQL::Test::Utils::tempdir_short(); + +my @scenario = ( + { + 'path' => $node->data_dir, + 'is_archive' => 0, + 'enabled' => 1 + }, + { + 'path' => "$tmp_dir/pg_wal.tar", + 'compression_method' => 'none', + 'compression_flags' => '-cf', + 'is_archive' => 1, + 'enabled' => 1 + }, + { + 'path' => "$tmp_dir/pg_wal.tar.gz", + 'compression_method' => 'gzip', + 'compression_flags' => '-czf', + 'is_archive' => 1, + 'enabled' => check_pg_config("#define HAVE_LIBZ 1") + }); + +for my $scenario (@scenario) +{ + my $path = $scenario->{'path'}; + + SKIP: + { + skip "tar command is not available", 3 + if !defined $tar; + skip "$scenario->{'compression_method'} compression not supported by this build", 3 + if !$scenario->{'enabled'} && $scenario->{'is_archive'}; + + # create pg_wal archive + if ($scenario->{'is_archive'}) + { + generate_archive($path, + $node->data_dir . '/pg_wal', + $scenario->{'compression_flags'}); + } + + command_fails_like( + [ 'pg_waldump', '--path' => $path ], + qr/error: no start WAL location given/, + 'path option requires start location'); + command_like( + [ + 'pg_waldump', + '--path' => $path, + '--start' => $start_lsn, + '--end' => $end_lsn, + ], + qr/./, + 'runs with path option and start and end locations'); + command_fails_like( + [ + 'pg_waldump', + '--path' => $path, + '--start' => $start_lsn, + ], + qr/error: error in WAL record at/, + 'falling off the end of the WAL results in an error'); + + command_fails_like( + [ + 'pg_waldump', '--quiet', + '--path' => $path, + '--start' => $start_lsn + ], + qr/error: error in WAL record at/, + 'errors are shown with --quiet'); + + my @lines; + @lines = test_pg_waldump($path); + is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines'); + + @lines = test_pg_waldump($path, '--limit' => 6); + is(@lines, 6, 'limit option observed'); + + @lines = test_pg_waldump($path, '--fullpage'); + is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW'); + + @lines = test_pg_waldump($path, '--stats'); + like($lines[0], qr/WAL statistics/, "statistics on stdout"); + is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output'); + + @lines = test_pg_waldump($path, '--stats=record'); + like($lines[0], qr/WAL statistics/, "statistics on stdout"); + is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output'); + + @lines = test_pg_waldump($path, '--rmgr' => 'Btree'); + is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines'); + + @lines = test_pg_waldump($path, '--fork' => 'init'); + is(grep(!/fork init/, @lines), 0, 'only init fork lines'); + + @lines = test_pg_waldump($path, + '--relation' => "$default_ts_oid/$postgres_db_oid/$rel_t1_oid"); + is(grep(!/rel $default_ts_oid\/$postgres_db_oid\/$rel_t1_oid/, @lines), + 0, 'only lines for selected relation'); + + @lines = test_pg_waldump($path, + '--relation' => "$default_ts_oid/$postgres_db_oid/$rel_i1a_oid", + '--block' => 1); + is(grep(!/\bblk 1\b/, @lines), 0, 'only lines for selected block'); + + # Cleanup. + unlink $path if $scenario->{'is_archive'}; + } +} done_testing(); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ac2da4c98cfc..677aa3b477ed 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3453,6 +3453,7 @@ astreamer_recovery_injector astreamer_tar_archiver astreamer_tar_parser astreamer_verify +astreamer_waldump astreamer_zstd_frame auth_password_hook_typ autovac_table