pg_waldump: support decoding of WAL inside tarfile
Hi All,
Attaching patch to support a new feature that let pg_waldump decode
WAL files directly from a tar archive. This was worked to address a
limitation in pg_verifybackup[1], which couldn't parse WAL files from
tar-formatted backups.
The implementation will align with pg_waldump's existing xlogreader
design, which uses three callback functions to manage WAL segments:
open, read, and close. For tar archives, however, the approach will be
simpler. Instead of using separate callbacks for opening and closing,
the tar archive will be opened once at the start and closed explicitly
at the end.
The core logic will be in the WAL page reading callback. When
xlogreader requests a new WAL page, this callback will be invoked. It
will then call the archive streamer routine to read the WAL data from
the tar archive into a buffer. This data will then be copied into
xlogreader's own buffer, completing the read.
Essentially, this is plumbing work: the new code will be responsible
for getting WAL data from the tar archive and feeding it to the
existing xlogreader. All other WAL page and record decoding logic,
which is already robust within xlogreader, will be reused as is.
This feature is being implemented in a series of patches as:
- Refactoring: The first few patches (0001-0004) are dedicated to
refactoring and minor code changes.
- 005: This patch introduces the core functionality for pg_waldump to
read WAL from a tar archive using the same archive streamer
(fe_utils/astreamer.h) used in pg_verifybackup. This version requires
WAL files in the archive to be in sequential order.
- 006: This patch removes the sequential order restriction. If
pg_waldump encounters an out-of-order WAL file, it writes the file to
a temporary directory. The utility will then continue decoding and
read from this temporary location later.
- 007 and onwards: These patches will update pg_verifybackup to remove the
restriction on WAL parsing for tar-formatted backups. 008 patch renames the
"--wal-directory" switch to "--wal-path" to make it more generic, allowing
it accepts a directory path or a tar archive path.
-----------------------------------
Known Issues & Status:
-----------------------------------
- Timeline Switching: The current implementation in patch 006 does not
correctly handle timeline switching. This is a known issue, especially
when a timeline change occurs on a WAL file that has been written to a
temporary location.
- Testing: Local regression tests on CentOS and macOS M4 are passing.
However, some tests on macOS Sonoma (specifically 008_untar.pl and
010_client_untar.pl) are failing in the GitHub workflow with a "WAL
parsing failed for timeline 1" error. This issue is currently being
investigated.
Please take a look at the attached patch and let me know your
thoughts. This is an initial version, and I am making incremental
improvements to address known issues and limitations.
1] https://git.postgresql.org/pg/commitdiff/8dfd3129027969fdd2d9d294220c867d2efd84aa
--
Regards,
Amul Sul
EDB: http://www.enterprisedb.com
Attachments:
v1-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchapplication/x-patch; name=v1-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchDownload
From 420ab4e05566f81fb15488ae7060b9d5648994b5 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 24 Jun 2025 11:33:20 +0530
Subject: [PATCH v1 1/9] Refactor: pg_waldump: Move some declarations to new
pg_waldump.h
This is in preparation for adding a second source file to this
directory.
---
src/bin/pg_waldump/pg_waldump.c | 11 ++---------
src/bin/pg_waldump/pg_waldump.h | 27 +++++++++++++++++++++++++++
2 files changed, 29 insertions(+), 9 deletions(-)
create mode 100644 src/bin/pg_waldump/pg_waldump.h
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 13d3ec2f5be..a49b2fd96c7 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,19 +40,11 @@
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};
-typedef struct XLogDumpPrivate
-{
- TimeLineID timeline;
- XLogRecPtr startptr;
- XLogRecPtr endptr;
- bool endptr_reached;
-} XLogDumpPrivate;
-
typedef struct XLogDumpConfig
{
/* display options */
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
new file mode 100644
index 00000000000..cd9a36d7447
--- /dev/null
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -0,0 +1,27 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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"
+
+extern int WalSegSz;
+
+/* Contains the necessary information to drive WAL decoding */
+typedef struct XLogDumpPrivate
+{
+ TimeLineID timeline;
+ XLogRecPtr startptr;
+ XLogRecPtr endptr;
+ bool endptr_reached;
+} XLogDumpPrivate;
+
+#endif /* end of PG_WALDUMP_H */
--
2.47.1
v1-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchapplication/x-patch; name=v1-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchDownload
From 30a226b1ae5ce3d1460bb3359c96a4e9a93d6b31 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 26 Jun 2025 11:42:53 +0530
Subject: [PATCH v1 2/9] Refactor: pg_waldump: Separate logic used to calculate
the required read size.
This refactoring prepares the codebase for an upcoming patch that will
support reading WAL from tar files. The logic for calculating the
required read size has been updated to handle both normal WAL files
and WAL files located inside a tar archive.
---
src/bin/pg_waldump/pg_waldump.c | 39 ++++++++++++++++++++++-----------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index a49b2fd96c7..8d0cd9e7156 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -326,6 +326,29 @@ identify_target_directory(char *directory, char *fname)
return NULL; /* not reached */
}
+/* 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,
@@ -383,21 +406,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))
--
2.47.1
v1-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchapplication/x-patch; name=v1-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchDownload
From 9e9121433ff4394238698bd27b3411daace5fd86 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 30 Jul 2025 12:43:30 +0530
Subject: [PATCH v1 3/9] Refactor: pg_waldump: Restructure TAP tests.
Restructured some tests to run inside a loop, facilitating their
re-execution for decoding WAL from tar archives.
---
src/bin/pg_waldump/t/001_basic.pl | 123 ++++++++++++++++--------------
1 file changed, 67 insertions(+), 56 deletions(-)
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index f26d75e01cf..1b712e8d74d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -198,28 +198,6 @@ command_like(
],
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 +205,6 @@ command_like(
],
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.
@@ -272,7 +241,6 @@ sub test_pg_waldump
my $result = IPC::Run::run [
'pg_waldump',
- '--path' => $node->data_dir,
'--start' => $start_lsn,
'--end' => $end_lsn,
@opts
@@ -288,38 +256,81 @@ sub test_pg_waldump
my @lines;
-@lines = test_pg_waldump;
-is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
+my @scenario = (
+ {
+ 'path' => $node->data_dir
+ });
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenario)
+{
+ my $path = $scenario->{'path'};
-@lines = test_pg_waldump('--fullpage');
-is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
+ SKIP:
+ {
+ 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');
-@lines = test_pg_waldump('--stats');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+ command_fails_like(
+ [
+ 'pg_waldump', '--quiet',
+ '--path' => $path,
+ '--start' => $start_lsn
+ ],
+ qr/error: error in WAL record at/,
+ 'errors are shown with --quiet');
-@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('--path' => $path);
+ is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
-@lines = test_pg_waldump('--rmgr' => 'Btree');
-is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+ @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ is(@lines, 6, 'limit option observed');
-@lines = test_pg_waldump('--fork' => 'init');
-is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+ @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
-@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('--path' => $path, '--stats');
+ like($lines[0], qr/WAL statistics/, "statistics on stdout");
+ is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
-@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');
+ @lines = test_pg_waldump('--path' => $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' => $path, '--rmgr' => 'Btree');
+ is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+
+ @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+
+ @lines = test_pg_waldump('--path' => $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' => $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');
+ }
+}
done_testing();
--
2.47.1
v1-0004-pg_waldump-Rename-directory-creation-routine-for-.patchapplication/x-patch; name=v1-0004-pg_waldump-Rename-directory-creation-routine-for-.patchDownload
From 58062914fb56aa4f8e005dbd24e072251f3150b6 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 29 Jul 2025 14:59:01 +0530
Subject: [PATCH v1 4/9] pg_waldump: Rename directory creation routine for
generalized use.
The create_fullpage_directory() function, currently used only for
storing full-page images from WAL records, should be renamed to a more
generalized name. This would allow it to be reused in future patches
for creating other directories as needed.
---
src/bin/pg_waldump/pg_waldump.c | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 8d0cd9e7156..4775275c07a 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -114,11 +114,11 @@ verify_directory(const char *directory)
}
/*
- * Create if necessary the directory storing the full-page images extracted
- * from the WAL records read.
+ * Create the directory if it doesn't exist. Report an error if creation fails
+ * or if an existing directory is not empty.
*/
static void
-create_fullpage_directory(char *path)
+create_directory(char *path)
{
int ret;
@@ -1112,8 +1112,12 @@ main(int argc, char **argv)
}
}
+ /*
+ * Create if necessary the directory storing the full-page images
+ * extracted from the WAL records read.
+ */
if (config.save_fullpage_path != NULL)
- create_fullpage_directory(config.save_fullpage_path);
+ create_directory(config.save_fullpage_path);
/* parse files as start/end boundaries, extract path if not specified */
if (optind < argc)
--
2.47.1
v1-0005-pg_waldump-Add-support-for-archived-WAL-decoding.patchapplication/x-patch; name=v1-0005-pg_waldump-Add-support-for-archived-WAL-decoding.patchDownload
From 21d9d604ca4b4ab08c5bc32decf1afc8d881c43c Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 18:37:59 +0530
Subject: [PATCH v1 5/9] pg_waldump: Add support for archived WAL decoding.
pg_waldump can now accept the path to a tar archive containing WAL
files and decode them. This feature was added primarily for
pg_verifybackup, which previously disabled WAL parsing for
tar-formatted backups.
Note that this patch requires that the WAL files within the archive be
in sequential order; an error will be reported otherwise. The next
patch is planned to remove this restriction.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/Makefile | 7 +-
src/bin/pg_waldump/astreamer_waldump.c | 378 +++++++++++++++++++++++++
src/bin/pg_waldump/meson.build | 4 +-
src/bin/pg_waldump/pg_waldump.c | 361 +++++++++++++++++++----
src/bin/pg_waldump/pg_waldump.h | 21 +-
src/bin/pg_waldump/t/001_basic.pl | 64 ++++-
src/tools/pgindent/typedefs.list | 1 +
8 files changed, 765 insertions(+), 79 deletions(-)
create mode 100644 src/bin/pg_waldump/astreamer_waldump.c
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index ce23add5577..d004bb0f67e 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -141,13 +141,17 @@ PostgreSQL documentation
<term><option>--path=<replaceable>path</replaceable></option></term>
<listitem>
<para>
- Specifies a directory to search for WAL segment files or a
- directory with a <literal>pg_wal</literal> subdirectory that
+ Specifies a tar archive or a directory to search for WAL segment files
+ or a directory with a <literal>pg_wal</literal> subdirectory that
contains such files. The default is to search in the current
directory, the <literal>pg_wal</literal> subdirectory of the
current directory, and the <literal>pg_wal</literal> subdirectory
of <envar>PGDATA</envar>.
</para>
+ <para>
+ If a tar archive is provided, its WAL segment files must be in
+ sequential order; otherwise, an error will be reported.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile
index 4c1ee649501..b234613eb50 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 00000000000..d0ac903c54e
--- /dev/null
+++ b/src/bin/pg_waldump/astreamer_waldump.c
@@ -0,0 +1,378 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 <unistd.h>
+
+#include "access/xlog_internal.h"
+#include "access/xlogdefs.h"
+#include "common/logging.h"
+#include "fe_utils/simple_list.h"
+#include "pg_waldump.h"
+
+/*
+ * How many bytes should we try to read from a file at once?
+ */
+#define READ_CHUNK_SIZE (128 * 1024)
+
+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;
+ XLogSegNo nextSegNo; /* Next expected segment to stream */
+} astreamer_waldump;
+
+static int astreamer_archive_read(XLogDumpPrivate *privateInfo);
+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_member *member,
+ TimeLineID startTimeLineID,
+ XLogSegNo startSegNo,
+ XLogSegNo endSegNo,
+ XLogSegNo nextSegNo,
+ XLogSegNo *curSegNo,
+ TimeLineID *curSegTimeline);
+
+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;
+
+ /*
+ * Ignore existing data if the required target page has not yet been
+ * read.
+ */
+ if (recptr >= endPtr)
+ {
+ len = 0;
+
+ /* Reset the buffer */
+ 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)
+ break; /* No data remaining */
+ }
+ }
+
+ return (count - nbytes) ? (count - nbytes) : -1;
+}
+
+/*
+ * Reads the archive and passes it to the archive streamer for decompression.
+ */
+static 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_content_new(astreamer *next, XLogRecPtr startptr,
+ XLogRecPtr endPtr, XLogDumpPrivate *privateInfo)
+{
+ astreamer_waldump *streamer;
+
+ streamer = palloc0(sizeof(astreamer_waldump));
+ *((const astreamer_ops **) &streamer->base.bbs_ops) =
+ &astreamer_waldump_ops;
+
+ streamer->base.bbs_next = next;
+ initStringInfo(&streamer->base.bbs_buffer);
+
+ if (XLogRecPtrIsInvalid(startptr))
+ streamer->startSegNo = 0;
+ else
+ {
+ XLByteToSeg(startptr, streamer->startSegNo, WalSegSz);
+
+ /*
+ * Initialize the record pointer to the beginning of the first
+ * segment; this pointer will track the WAL record reading status.
+ */
+ XLogSegNoOffsetToRecPtr(streamer->startSegNo, 0, WalSegSz,
+ privateInfo->archive_streamer_read_ptr);
+ }
+
+ 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:
+ {
+ XLogSegNo segNo;
+ TimeLineID timeline;
+
+ pg_log_debug("pg_waldump: reading \"%s\"", member->pathname);
+
+ mystreamer->skipThisSeg = false;
+
+ if (!member_is_relevant_wal(member,
+ privateInfo->timeline,
+ mystreamer->startSegNo,
+ mystreamer->endSegNo,
+ mystreamer->nextSegNo,
+ &segNo, &timeline))
+ {
+ mystreamer->skipThisSeg = true;
+ break;
+ }
+
+ /*
+ * If nextSegNo is 0, the check is skipped, and any WAL file
+ * can be read -- this typically occurs during initial
+ * verification.
+ */
+ if (mystreamer->nextSegNo == 0)
+ break;
+
+ /* WAL segments must be archived in order */
+ if (mystreamer->nextSegNo != segNo)
+ {
+ pg_log_error("WAL files are not archived in sequential order");
+ pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
+ mystreamer->nextSegNo, segNo);
+ exit(1);
+ }
+
+ /*
+ * We track the reading of WAL segment records using a pointer
+ * that's continuously incremented by the length of the
+ * received data. This pointer is crucial for serving WAL page
+ * requests from the WAL decoding routine, so it must be
+ * accurate.
+ */
+#ifdef USE_ASSERT_CHECKING
+ if (mystreamer->nextSegNo != 0)
+ {
+ XLogRecPtr recPtr;
+
+ XLogSegNoOffsetToRecPtr(segNo, 0, WalSegSz, recPtr);
+ Assert(privateInfo->archive_streamer_read_ptr == recPtr);
+ }
+#endif
+
+ /* Save the timeline */
+ privateInfo->timeline = timeline;
+
+ /* Update the next expected segment number */
+ mystreamer->nextSegNo += 1;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_CONTENTS:
+ /* Skip this segment */
+ if (mystreamer->skipThisSeg)
+ break;
+
+ /* Or, copy contents to buffer */
+ privateInfo->archive_streamer_read_ptr += len;
+ astreamer_buffer_bytes(streamer, &data, &len, len);
+ break;
+
+ case ASTREAMER_MEMBER_TRAILER:
+ 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)
+{
+ Assert(streamer->bbs_next == NULL);
+
+ 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_member *member, TimeLineID startTimeLineID,
+ XLogSegNo startSegNo, XLogSegNo endSegNo,
+ XLogSegNo nextSegNo, XLogSegNo *curSegNo,
+ TimeLineID *curSegTimeline)
+{
+ 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);
+
+ /* Ignore the older timeline */
+ if (startTimeLineID > timeline)
+ return false;
+
+ /* Skip if the current segment is not the desired one */
+ if (startSegNo > segNo || endSegNo < segNo)
+ return false;
+
+ *curSegNo = segNo;
+ *curSegTimeline = timeline;
+
+ return true;
+}
diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build
index 937e0d68841..2a0300dc339 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 4775275c07a..64f3a65b735 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -182,10 +182,9 @@ open_file_in_directory(const char *directory, const char *fname)
{
int fd = -1;
char fpath[MAXPGPATH];
+ char *dir = directory ? (char *) directory : ".";
- Assert(directory != NULL);
-
- snprintf(fpath, MAXPGPATH, "%s/%s", directory, fname);
+ snprintf(fpath, MAXPGPATH, "%s/%s", dir, fname);
fd = open(fpath, O_RDONLY | PG_BINARY, 0);
if (fd < 0 && errno != ENOENT)
@@ -326,6 +325,160 @@ 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;
+}
+
+/*
+ * Creates an appropriate chain of archive streamers for reading the given
+ * tar archive.
+ */
+static void
+setup_astreamer(XLogDumpPrivate *private, pg_compress_algorithm compression,
+ XLogRecPtr startptr, XLogRecPtr endptr)
+{
+ astreamer *streamer = NULL;
+
+ streamer = astreamer_waldump_content_new(NULL, 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;
+}
+
+/*
+ * Initializes the archive reader for a tar file.
+ */
+static void
+init_tar_archive_reader(XLogDumpPrivate *private, char *waldir,
+ pg_compress_algorithm compression)
+{
+ int fd;
+
+ /* Now, the 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;
+
+ /* Setup tar archive reading facility */
+ setup_astreamer(private, compression, private->startptr, private->endptr);
+}
+
+/*
+ * 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;
+
+ setup_astreamer(private, compression, InvalidXLogRecPtr, InvalidXLogRecPtr);
+
+ /* Now, the tar archive and store its file descriptor */
+ private->archive_fd = open_file_in_directory(waldir, private->archive_name);
+
+ if (private->archive_fd < 0)
+ pg_fatal("could not open file \"%s\"", private->archive_name);
+
+ /* 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,
@@ -406,7 +559,7 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPagePtr, reqLen);
+ int count = required_read_len(private, targetPtr, reqLen);
WALReadError errinfo;
if (private->endptr_reached)
@@ -436,6 +589,44 @@ 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, targetPtr, reqLen);
+
+ if (private->endptr_reached)
+ return -1;
+
+ /* 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.
@@ -773,8 +964,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"
@@ -806,7 +997,11 @@ main(int argc, char **argv)
XLogRecord *record;
XLogRecPtr first_record;
char *waldir = NULL;
+ char *walpath = NULL;
char *errormsg;
+ bool is_tar = false;
+ XLogReaderRoutine *routine = NULL;
+ pg_compress_algorithm compression;
static struct option long_options[] = {
{"bkp-details", no_argument, NULL, 'b'},
@@ -938,7 +1133,7 @@ main(int argc, char **argv)
}
break;
case 'p':
- waldir = pg_strdup(optarg);
+ walpath = pg_strdup(optarg);
break;
case 'q':
config.quiet = true;
@@ -1102,10 +1297,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;
@@ -1129,44 +1334,23 @@ main(int argc, char **argv)
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;
+ waldir = walpath;
+ 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)
@@ -1174,32 +1358,67 @@ 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)
+ verify_tar_archive(&private, waldir, compression);
/* we don't know what to print */
if (XLogRecPtrIsInvalid(private.startptr))
@@ -1211,11 +1430,26 @@ main(int argc, char **argv)
/* done with argument parsing, do the actual work */
/* we have everything we need, start reading */
+ if (is_tar)
+ {
+ /* Set up for reading tar file */
+ init_tar_archive_reader(&private, waldir, compression);
+
+ /* Routine to decode WAL files in tar archive */
+ routine = XL_ROUTINE(.page_read = TarWALDumpReadPage,
+ .segment_open = TarWALDumpOpenSegment,
+ .segment_close = TarWALDumpCloseSegment);
+ }
+ else
+ {
+ /* Routine to decode WAL files */
+ routine = XL_ROUTINE(.page_read = WALDumpReadPage,
+ .segment_open = WALDumpOpenSegment,
+ .segment_close = WALDumpCloseSegment);
+ }
+
xlogreader_state =
- XLogReaderAllocate(WalSegSz, waldir,
- XL_ROUTINE(.page_read = WALDumpReadPage,
- .segment_open = WALDumpOpenSegment,
- .segment_close = WALDumpCloseSegment),
+ XLogReaderAllocate(WalSegSz, waldir, routine,
&private);
if (!xlogreader_state)
pg_fatal("out of memory while allocating a WAL reading processor");
@@ -1325,6 +1559,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
index cd9a36d7447..d2c2307d6c2 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -12,6 +12,8 @@
#define PG_WALDUMP_H
#include "access/xlogdefs.h"
+#include "fe_utils/astreamer.h"
+#include "lib/stringinfo.h"
extern int WalSegSz;
@@ -22,6 +24,23 @@ typedef struct XLogDumpPrivate
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 */
} XLogDumpPrivate;
-#endif /* end of PG_WALDUMP_H */
+
+extern astreamer *astreamer_waldump_content_new(astreamer *next,
+ XLogRecPtr startptr,
+ XLogRecPtr endptr,
+ XLogDumpPrivate *privateInfo);
+extern int astreamer_wal_read(char *readBuff, XLogRecPtr startptr, Size count,
+ 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 1b712e8d74d..80298d2a51d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -3,10 +3,13 @@
use strict;
use warnings FATAL => 'all';
+use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $tar = $ENV{TAR};
+
program_help_ok('pg_waldump');
program_version_ok('pg_waldump');
program_options_handling_ok('pg_waldump');
@@ -235,7 +238,7 @@ command_like(
sub test_pg_waldump
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my @opts = @_;
+ my ($path, @opts) = @_;
my ($stdout, $stderr);
@@ -243,6 +246,7 @@ sub test_pg_waldump
'pg_waldump',
'--start' => $start_lsn,
'--end' => $end_lsn,
+ '--path' => $path,
@opts
],
'>' => \$stdout,
@@ -254,11 +258,27 @@ sub test_pg_waldump
return @lines;
}
-my @lines;
+my $tmp_dir = PostgreSQL::Test::Utils::tempdir_short();
my @scenario = (
{
- 'path' => $node->data_dir
+ '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)
@@ -267,6 +287,22 @@ for my $scenario (@scenario)
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'})
+ {
+ # move into the WAL directory before archiving files
+ my $cwd = getcwd;
+ chdir($node->data_dir . '/pg_wal/') || die "chdir: $!";
+ command_ok(
+ [ $tar, $scenario->{'compression_flags'}, $path , '.' ]);
+ chdir($cwd) || die "chdir: $!";
+ }
+
command_fails_like(
[ 'pg_waldump', '--path' => $path ],
qr/error: no start WAL location given/,
@@ -298,38 +334,42 @@ for my $scenario (@scenario)
qr/error: error in WAL record at/,
'errors are shown with --quiet');
- @lines = test_pg_waldump('--path' => $path);
+ my @lines;
+ @lines = test_pg_waldump($path);
is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
- @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ @lines = test_pg_waldump($path, '--limit' => 6);
is(@lines, 6, 'limit option observed');
- @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ @lines = test_pg_waldump($path, '--fullpage');
is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
- @lines = test_pg_waldump('--path' => $path, '--stats');
+ @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' => $path, '--stats=record');
+ @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' => $path, '--rmgr' => 'Btree');
+ @lines = test_pg_waldump($path, '--rmgr' => 'Btree');
is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
- @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ @lines = test_pg_waldump($path, '--fork' => 'init');
is(grep(!/fork init/, @lines), 0, 'only init fork lines');
- @lines = test_pg_waldump('--path' => $path,
+ @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' => $path,
+ @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'};
}
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e6f2e93b2d6..d8428ce2352 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3445,6 +3445,7 @@ astreamer_recovery_injector
astreamer_tar_archiver
astreamer_tar_parser
astreamer_verify
+astreamer_waldump
astreamer_zstd_frame
auth_password_hook_typ
autovac_table
--
2.47.1
v1-0006-WIP-pg_waldump-Remove-the-restriction-on-the-orde.patchapplication/x-patch; name=v1-0006-WIP-pg_waldump-Remove-the-restriction-on-the-orde.patchDownload
From 7469b7b6bf3dd84d092fd86f69bf5ab574ee4f85 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 7 Aug 2025 17:37:23 +0530
Subject: [PATCH v1 6/9] WIP-pg_waldump: Remove the restriction on the order of
archived WAL files.
With previous patch, pg_waldump would stop decoding if WAL files were
not in the required sequence. With this patch, decoding will now
continue. Any WAL file that is out of order will be written to a
temporary location, from which it will be read later. Once a temporary
file has been read, it will be removed.
TODO:
Timeline switching is not handled correctly, especially when a
timeline change occurs on the next WAL file that was previously
written to a temporary location.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/astreamer_waldump.c | 188 +++++++++++++++++++++----
src/bin/pg_waldump/pg_waldump.c | 99 ++++++++++++-
src/bin/pg_waldump/pg_waldump.h | 1 +
src/bin/pg_waldump/t/001_basic.pl | 40 +++++-
5 files changed, 301 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index d004bb0f67e..8a28b4f0f91 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -149,8 +149,12 @@ PostgreSQL documentation
of <envar>PGDATA</envar>.
</para>
<para>
- If a tar archive is provided, its WAL segment files must be in
- sequential order; otherwise, an error will be reported.
+ If a tar archive is provided and its WAL segment files are not in
+ sequential order, those files will be written to a temporary directory
+ named <filename>pg_waldump_tmp_dir/</filename>. This directory will be
+ created inside the directory specified by the <envar>TMPDIR</envar>
+ environment variable if it is set; otherwise, it will be created within
+ the same directory as the tar archive.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/astreamer_waldump.c b/src/bin/pg_waldump/astreamer_waldump.c
index d0ac903c54e..a088c33b16f 100644
--- a/src/bin/pg_waldump/astreamer_waldump.c
+++ b/src/bin/pg_waldump/astreamer_waldump.c
@@ -18,6 +18,7 @@
#include "access/xlog_internal.h"
#include "access/xlogdefs.h"
+#include "common/file_perm.h"
#include "common/logging.h"
#include "fe_utils/simple_list.h"
#include "pg_waldump.h"
@@ -37,6 +38,9 @@ typedef struct astreamer_waldump
/* These fields change with archive member. */
bool skipThisSeg;
+ bool writeThisSeg;
+ FILE *segFp;
+ SimpleStringList exportedSegList; /* Temporary exported segment list */
XLogSegNo nextSegNo; /* Next expected segment to stream */
} astreamer_waldump;
@@ -53,8 +57,11 @@ static bool member_is_relevant_wal(astreamer_member *member,
XLogSegNo startSegNo,
XLogSegNo endSegNo,
XLogSegNo nextSegNo,
+ char **curFname,
XLogSegNo *curSegNo,
TimeLineID *curSegTimeline);
+static bool member_needs_temp_write(astreamer_waldump *mystreamer,
+ const char *fname);
static const astreamer_ops astreamer_waldump_ops = {
.content = astreamer_waldump_content,
@@ -189,17 +196,8 @@ astreamer_waldump_content_new(astreamer *next, XLogRecPtr startptr,
if (XLogRecPtrIsInvalid(startptr))
streamer->startSegNo = 0;
else
- {
XLByteToSeg(startptr, streamer->startSegNo, WalSegSz);
- /*
- * Initialize the record pointer to the beginning of the first
- * segment; this pointer will track the WAL record reading status.
- */
- XLogSegNoOffsetToRecPtr(streamer->startSegNo, 0, WalSegSz,
- privateInfo->archive_streamer_read_ptr);
- }
-
if (XLogRecPtrIsInvalid(endPtr))
streamer->endSegNo = UINT64_MAX;
else
@@ -228,19 +226,21 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
{
case ASTREAMER_MEMBER_HEADER:
{
+ char *fname;
XLogSegNo segNo;
TimeLineID timeline;
pg_log_debug("pg_waldump: reading \"%s\"", member->pathname);
mystreamer->skipThisSeg = false;
+ mystreamer->writeThisSeg = false;
if (!member_is_relevant_wal(member,
privateInfo->timeline,
mystreamer->startSegNo,
mystreamer->endSegNo,
mystreamer->nextSegNo,
- &segNo, &timeline))
+ &fname, &segNo, &timeline))
{
mystreamer->skipThisSeg = true;
break;
@@ -254,24 +254,37 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
if (mystreamer->nextSegNo == 0)
break;
- /* WAL segments must be archived in order */
- if (mystreamer->nextSegNo != segNo)
+ /*
+ * 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 != segNo &&
+ member_needs_temp_write(mystreamer, fname))
{
- pg_log_error("WAL files are not archived in sequential order");
- pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
- mystreamer->nextSegNo, segNo);
- exit(1);
+ mystreamer->writeThisSeg = true;
+ break;
}
/*
- * We track the reading of WAL segment records using a pointer
- * that's continuously incremented by the length of the
- * received data. This pointer is crucial for serving WAL page
- * requests from the WAL decoding routine, so it must be
- * accurate.
+ * 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.
+ * Also, if there's any existing data in the buffer, the next
+ * WAL record should logically follow it.
*/
#ifdef USE_ASSERT_CHECKING
- if (mystreamer->nextSegNo != 0)
+ Assert(!mystreamer->skipThisSeg);
+ Assert(!mystreamer->writeThisSeg);
+
+ if (privateInfo->archive_streamer_buf->len != 0)
{
XLogRecPtr recPtr;
@@ -280,6 +293,13 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
}
#endif
+ /*
+ * Initialized to the beginning of the current segment being
+ * streamed through the buffer.
+ */
+ XLogSegNoOffsetToRecPtr(segNo, 0, WalSegSz,
+ privateInfo->archive_streamer_read_ptr);
+
/* Save the timeline */
privateInfo->timeline = timeline;
@@ -293,12 +313,44 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
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/%s\": %m",
+ privateInfo->tmpdir, 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;
+ }
break;
case ASTREAMER_ARCHIVE_TRAILER:
@@ -325,8 +377,14 @@ astreamer_waldump_finalize(astreamer *streamer)
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);
}
@@ -339,8 +397,8 @@ astreamer_waldump_free(astreamer *streamer)
static bool
member_is_relevant_wal(astreamer_member *member, TimeLineID startTimeLineID,
XLogSegNo startSegNo, XLogSegNo endSegNo,
- XLogSegNo nextSegNo, XLogSegNo *curSegNo,
- TimeLineID *curSegTimeline)
+ XLogSegNo nextSegNo, char **curFname,
+ XLogSegNo *curSegNo, TimeLineID *curSegTimeline)
{
int pathlen;
XLogSegNo segNo;
@@ -371,8 +429,90 @@ member_is_relevant_wal(astreamer_member *member, TimeLineID startTimeLineID,
if (startSegNo > segNo || endSegNo < segNo)
return false;
+ /*
+ * A corner case where we've already streamed the contents of an archived
+ * WAL segment with a similar name, so ignoring this duplicate.
+ */
+ if (nextSegNo > segNo)
+ return false;
+
+ *curFname = fname;
*curSegNo = segNo;
*curSegTimeline = timeline;
return true;
}
+
+/*
+ * Returns true and creates a temporary file if the given WAL segment needs to
+ * be written to temporary space. This is required when the segment is not the
+ * one currently being decoded. Conversely, if a temporary file for the
+ * preceding segment already exists and the current segment is its direct
+ * successor, then writing to temporary space is not necessary, and false is
+ * returned.
+ */
+static bool
+member_needs_temp_write(astreamer_waldump *mystreamer, const char *fname)
+{
+ bool exists;
+ XLogSegNo segNo;
+ TimeLineID timeline;
+ XLogDumpPrivate *privateInfo = mystreamer->privateInfo;
+
+ /* Parse position from file */
+ XLogFromFileName(fname, &timeline, &segNo, WalSegSz);
+
+ /*
+ * 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 the current segment number matches the required WAL
+ * segment (i.e. nextSegNo). If it does, we allow it to stream normally
+ * through the buffer. Otherwise, we write it to the temporary space, from
+ * where the caller is expected to read it directly.
+ */
+ do
+ {
+ char segName[MAXFNAMELEN];
+
+ XLogFileName(segName, timeline, mystreamer->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(&mystreamer->exportedSegList, segName))
+ {
+ mystreamer->nextSegNo += 1;
+ exists = true;
+ }
+ } while (exists);
+
+ /*
+ * Need to export this segment to disk; create an empty placeholder file
+ * to be written once its content is received.
+ */
+ if (mystreamer->nextSegNo != segNo)
+ {
+ char fpath[MAXPGPATH];
+
+ snprintf(fpath, MAXPGPATH, "%s/%s", privateInfo->tmpdir, fname);
+
+ mystreamer->segFp = fopen(fpath, PG_BINARY_W);
+ if (mystreamer->segFp == 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
+
+ /* Record this segment's export to temporary space */
+ simple_string_list_append(&mystreamer->exportedSegList, fname);
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 64f3a65b735..54a3b2dacda 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -325,6 +325,51 @@ identify_target_directory(char *directory, char *fname)
return NULL; /* not reached */
}
+/*
+ * Set up a temporary directory to temporarily store WAL segments.
+ */
+static char *
+setup_tmp_dir(char *waldir)
+{
+ char *tmpdir = waldir != NULL ? pstrdup(waldir) : pstrdup(".");
+
+ canonicalize_path(tmpdir);
+ tmpdir = psprintf("%s/pg_waldump_tmp_dir",
+ getenv("TMPDIR") ? getenv("TMPDIR") : tmpdir);
+
+ create_directory(tmpdir);
+
+ return tmpdir;
+}
+
+/*
+ * Removes a directory along with its contents, if any.
+ */
+static void
+remove_tmp_dir(char *tmpdir)
+{
+ DIR *dir;
+ struct dirent *de;
+
+ dir = opendir(tmpdir);
+ while ((de = readdir(dir)) != NULL)
+ {
+ char path[MAXPGPATH];
+
+ if (strcmp(de->d_name, ".") == 0 ||
+ strcmp(de->d_name, "..") == 0)
+ continue;
+
+ snprintf(path, MAXPGPATH, "%s/%s", tmpdir, de->d_name);
+ unlink(path);
+ }
+ closedir(dir);
+
+ if (rmdir(tmpdir) < 0)
+ pg_log_error("could not remove directory \"%s\": %m",
+ tmpdir);
+}
+
/*
* Returns true if the given file is a tar archive and outputs its compression
* algorithm.
@@ -559,7 +604,7 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPtr, reqLen);
+ int count = required_read_len(private, targetPagePtr, reqLen);
WALReadError errinfo;
if (private->endptr_reached)
@@ -618,12 +663,53 @@ TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPtr, reqLen);
+ int count = required_read_len(private, targetPagePtr, reqLen);
+ XLogSegNo nextSegNo;
if (private->endptr_reached)
return -1;
- /* Read the WAL page from the archive streamer */
+ /*
+ * If the target page is in a different segment, first check for the WAL
+ * segment's physical existence in the temporary directory.
+ *
+ * XXX: Timeline change is not handled.
+ */
+ nextSegNo = state->seg.ws_segno;
+ if (!XLByteInSeg(targetPagePtr, nextSegNo, WalSegSz))
+ {
+ char fname[MAXPGPATH];
+
+ if (state->seg.ws_file >= 0)
+ {
+ char fpath[MAXPGPATH];
+
+ 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);
+ snprintf(fpath, MAXPGPATH, "%s/%s", private->tmpdir, fname);
+ unlink(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);
+ state->seg.ws_file = open_file_in_directory(private->tmpdir, fname);
+ }
+
+ /* Continue reading from the open WAL segment, if any */
+ if (state->seg.ws_file >= 0)
+ return WALDumpReadPage(state, targetPagePtr, reqLen, targetPtr,
+ readBuff);
+
+ /* Otherwise, read the WAL page from the archive streamer */
return astreamer_wal_read(readBuff, targetPagePtr, count, private);
}
@@ -1435,6 +1521,9 @@ main(int argc, char **argv)
/* Set up for reading tar file */
init_tar_archive_reader(&private, waldir, compression);
+ /* Create temporary space for writing WAL segments. */
+ private.tmpdir = setup_tmp_dir(waldir);
+
/* Routine to decode WAL files in tar archive */
routine = XL_ROUTINE(.page_read = TarWALDumpReadPage,
.segment_open = TarWALDumpOpenSegment,
@@ -1549,6 +1638,10 @@ main(int argc, char **argv)
if (config.stats == true && !config.quiet)
XLogDumpDisplayStats(&config, &stats);
+ /* Remove temporary directory if any */
+ if (private.tmpdir != NULL)
+ remove_tmp_dir(private.tmpdir);
+
if (time_to_stop)
exit(0);
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
index d2c2307d6c2..2644d847b47 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -33,6 +33,7 @@ typedef struct XLogDumpPrivate
StringInfo archive_streamer_buf; /* Buffer for receiving WAL data */
XLogRecPtr archive_streamer_read_ptr; /* Populate the buffer with records
until this record pointer */
+ char *tmpdir;
} XLogDumpPrivate;
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 80298d2a51d..a3bf950db97 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -7,6 +7,8 @@ use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+use File::Path qw(rmtree);
+use List::Util qw(shuffle);
my $tar = $ENV{TAR};
@@ -258,6 +260,32 @@ sub test_pg_waldump
return @lines;
}
+# Create a tar archive, shuffling 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: $!";
+
+ # give necessary permission
+ chmod(0755, $archive) || die "chmod $archive: $!";
+}
+
my $tmp_dir = PostgreSQL::Test::Utils::tempdir_short();
my @scenario = (
@@ -291,16 +319,16 @@ for my $scenario (@scenario)
if !defined $tar;
skip "$scenario->{'compression_method'} compression not supported by this build", 3
if !$scenario->{'enabled'} && $scenario->{'is_archive'};
+ skip "unix-style permissions not supported on Windows", 3
+ if ($scenario->{'is_archive'}
+ && ($windows_os || $Config::Config{osname} eq 'cygwin'));
# create pg_wal archive
if ($scenario->{'is_archive'})
{
- # move into the WAL directory before archiving files
- my $cwd = getcwd;
- chdir($node->data_dir . '/pg_wal/') || die "chdir: $!";
- command_ok(
- [ $tar, $scenario->{'compression_flags'}, $path , '.' ]);
- chdir($cwd) || die "chdir: $!";
+ generate_archive($path,
+ $node->data_dir . '/pg_wal',
+ $scenario->{'compression_flags'});
}
command_fails_like(
--
2.47.1
v1-0007-pg_verifybackup-Delay-default-WAL-directory-prepa.patchapplication/x-patch; name=v1-0007-pg_verifybackup-Delay-default-WAL-directory-prepa.patchDownload
From 10816f545e7f2f3df1fb9075321d2bd81df195d4 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 14:47:43 +0530
Subject: [PATCH v1 7/9] pg_verifybackup: Delay default WAL directory
preparation.
We are not sure whether to parse WAL from a directory or an archive
until the backup format is known. Therefore, we delay preparing the
default WAL directory until the point of parsing. This delay is
harmless, as the WAL directory is not used elsewhere.
---
src/bin/pg_verifybackup/pg_verifybackup.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 5e6c13bb921..31ebc1581fb 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -285,10 +285,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
@@ -368,6 +364,10 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
+ /* 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 parse the required ranges of WAL records, unless we were told
* not to do so.
--
2.47.1
v1-0008-pg_verifybackup-Rename-the-wal-directory-switch-t.patchapplication/x-patch; name=v1-0008-pg_verifybackup-Rename-the-wal-directory-switch-t.patchDownload
From c8187d4996df271117afb623db6c72d3033d4b06 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 24 Jul 2025 16:37:43 +0530
Subject: [PATCH v1 8/9] pg_verifybackup: Rename the wal-directory switch to
wal-path
Future patches to pg_waldump will enable it to decode WAL directly
from tar files. This means you'll be able to specify a tar archive
path instead of a traditional WAL directory.
To keep things consistent and more versatile, we should also
generalize the input switch for pg_verifybackup. It should accept
either a directory or a tar file path that contains WALs. This change
will also aligning it with the existing manifest-path switch naming.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 2 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 22 +++++++++++-----------
src/bin/pg_verifybackup/po/de.po | 4 ++--
src/bin/pg_verifybackup/po/el.po | 4 ++--
src/bin/pg_verifybackup/po/es.po | 4 ++--
src/bin/pg_verifybackup/po/fr.po | 4 ++--
src/bin/pg_verifybackup/po/it.po | 4 ++--
src/bin/pg_verifybackup/po/ja.po | 4 ++--
src/bin/pg_verifybackup/po/ka.po | 4 ++--
src/bin/pg_verifybackup/po/ko.po | 4 ++--
src/bin/pg_verifybackup/po/ru.po | 4 ++--
src/bin/pg_verifybackup/po/sv.po | 4 ++--
src/bin/pg_verifybackup/po/uk.po | 4 ++--
src/bin/pg_verifybackup/po/zh_CN.po | 4 ++--
src/bin/pg_verifybackup/po/zh_TW.po | 4 ++--
src/bin/pg_verifybackup/t/007_wal.pl | 4 ++--
16 files changed, 40 insertions(+), 40 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index 61c12975e4a..e9b8bfd51b1 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -261,7 +261,7 @@ PostgreSQL documentation
<varlistentry>
<term><option>-w <replaceable class="parameter">path</replaceable></option></term>
- <term><option>--wal-directory=<replaceable class="parameter">path</replaceable></option></term>
+ <term><option>--wal-path=<replaceable class="parameter">path</replaceable></option></term>
<listitem>
<para>
Try to parse WAL files stored in the specified directory, rather than
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 31ebc1581fb..1ee400199da 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -93,7 +93,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 +126,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 +135,7 @@ 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 *pg_waldump_path = NULL;
DIR *dir;
@@ -221,8 +221,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 */
@@ -365,15 +365,15 @@ main(int argc, char **argv)
verify_backup_checksums(&context);
/* By default, look for the WAL in the backup directory, too. */
- if (wal_directory == NULL)
- wal_directory = psprintf("%s/pg_wal", context.backup_directory);
+ if (wal_path == NULL)
+ wal_path = psprintf("%s/pg_wal", context.backup_directory);
/*
* 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
@@ -1198,7 +1198,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 +1208,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 +1376,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 a9e24931100..9b5cd5898cf 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 3e3f20c67c5..81442f51c17 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 0cb958f3448..7f729fa35ba 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 da8c72f6427..09937966fa7 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 317b0b71e7f..4da68d0074e 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 c910fb236cc..a948959b54f 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 982751984c7..ef2799316a8 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 acdc3da5e02..eaf91ef1e98 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 64005feedfd..7fb0e5ab1f6 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 17240feeb5c..97125838e8c 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 034b9764232..63f8041ab38 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 b7d97c8976d..fb6fcae8b82 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 c1b710b0a36..568f972b0bb 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/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl
index babc4f0a86b..b07f80719b0 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', '--no-parse-wal', $backup_path ],
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: $!";
--
2.47.1
v1-0009-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchapplication/x-patch; name=v1-0009-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchDownload
From 923a767b076e04c75f6472d2800a22ca99a31d53 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 17 Jul 2025 16:39:36 +0530
Subject: [PATCH v1 9/9] pg_verifybackup: enabled WAL parsing for tar-format
backup
Now that pg_waldump supports decoding from tar archives, we should
leverage this functionality to remove the previous restriction on WAL
parsing for tar-backed formats.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 5 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 66 +++++++++++++------
src/bin/pg_verifybackup/t/002_algorithm.pl | 4 --
src/bin/pg_verifybackup/t/003_corruption.pl | 4 +-
src/bin/pg_verifybackup/t/008_untar.pl | 3 +-
src/bin/pg_verifybackup/t/010_client_untar.pl | 3 +-
6 files changed, 50 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index e9b8bfd51b1..16b50b5a4df 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -36,10 +36,7 @@ PostgreSQL documentation
<literal>backup_manifest</literal> 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 <application>pg_basebackup</application>. However, at present,
- <literal>WAL</literal> verification is supported only for plain-format
- backups. Therefore, if the backup is stored in tar-format, the
- <literal>-n, --no-parse-wal</literal> option should be used.
+ supported by <application>pg_basebackup</application>.
</para>
<para>
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 1ee400199da..4bfe6fdff16 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);
@@ -136,6 +140,8 @@ main(int argc, char **argv)
bool no_parse_wal = false;
bool quiet = false;
char *wal_path = NULL;
+ char *base_archive_path = NULL;
+ char *wal_archive_path = NULL;
char *pg_waldump_path = NULL;
DIR *dir;
@@ -327,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'.
@@ -346,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.
@@ -364,9 +359,28 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
- /* By default, look for the WAL in the backup directory, too. */
+ /*
+ * 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)
- wal_path = psprintf("%s/pg_wal", context.backup_directory);
+ {
+ 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
@@ -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 <tablespaceoid>.tar file here. */
diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl
index ae16c11bc4d..4f284a9e828 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 1dd60f709cf..f1ebdbb46b4 100644
--- a/src/bin/pg_verifybackup/t/003_corruption.pl
+++ b/src/bin/pg_verifybackup/t/003_corruption.pl
@@ -193,10 +193,8 @@ for my $scenario (@scenario)
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/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl
index bc3d6b352ad..0cfe1f9532c 100644
--- a/src/bin/pg_verifybackup/t/008_untar.pl
+++ b/src/bin/pg_verifybackup/t/008_untar.pl
@@ -123,8 +123,7 @@ for my $tc (@test_configuration)
# 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 b62faeb5acf..76269a73673 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 @@ for my $tc (@test_configuration)
# 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");
--
2.47.1
On Thu, Aug 7, 2025 at 7:47 PM Amul Sul <sulamul@gmail.com> wrote:
[....]
-----------------------------------
Known Issues & Status:
-----------------------------------
- Timeline Switching: The current implementation in patch 006 does not
correctly handle timeline switching. This is a known issue, especially
when a timeline change occurs on a WAL file that has been written to a
temporary location.
This is still pending and will be addressed in the next version.
Therefore, patch 0006 remains marked as WIP.
- Testing: Local regression tests on CentOS and macOS M4 are passing.
However, some tests on macOS Sonoma (specifically 008_untar.pl and
010_client_untar.pl) are failing in the GitHub workflow with a "WAL
parsing failed for timeline 1" error. This issue is currently being
investigated.
This has been fixed in the attached version; all GitHub workflow tests
are now fine.
Regards,
Amul
Attachments:
v2-0008-pg_verifybackup-Rename-the-wal-directory-switch-t.patchapplication/x-patch; name=v2-0008-pg_verifybackup-Rename-the-wal-directory-switch-t.patchDownload
From 49d74dca63e15c300a8ccf317d17003f6f9412e8 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 24 Jul 2025 16:37:43 +0530
Subject: [PATCH v2 8/9] pg_verifybackup: Rename the wal-directory switch to
wal-path
Future patches to pg_waldump will enable it to decode WAL directly
from tar files. This means you'll be able to specify a tar archive
path instead of a traditional WAL directory.
To keep things consistent and more versatile, we should also
generalize the input switch for pg_verifybackup. It should accept
either a directory or a tar file path that contains WALs. This change
will also aligning it with the existing manifest-path switch naming.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 2 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 22 +++++++++++-----------
src/bin/pg_verifybackup/po/de.po | 4 ++--
src/bin/pg_verifybackup/po/el.po | 4 ++--
src/bin/pg_verifybackup/po/es.po | 4 ++--
src/bin/pg_verifybackup/po/fr.po | 4 ++--
src/bin/pg_verifybackup/po/it.po | 4 ++--
src/bin/pg_verifybackup/po/ja.po | 4 ++--
src/bin/pg_verifybackup/po/ka.po | 4 ++--
src/bin/pg_verifybackup/po/ko.po | 4 ++--
src/bin/pg_verifybackup/po/ru.po | 4 ++--
src/bin/pg_verifybackup/po/sv.po | 4 ++--
src/bin/pg_verifybackup/po/uk.po | 4 ++--
src/bin/pg_verifybackup/po/zh_CN.po | 4 ++--
src/bin/pg_verifybackup/po/zh_TW.po | 4 ++--
src/bin/pg_verifybackup/t/007_wal.pl | 4 ++--
16 files changed, 40 insertions(+), 40 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index 61c12975e4a..e9b8bfd51b1 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -261,7 +261,7 @@ PostgreSQL documentation
<varlistentry>
<term><option>-w <replaceable class="parameter">path</replaceable></option></term>
- <term><option>--wal-directory=<replaceable class="parameter">path</replaceable></option></term>
+ <term><option>--wal-path=<replaceable class="parameter">path</replaceable></option></term>
<listitem>
<para>
Try to parse WAL files stored in the specified directory, rather than
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 31ebc1581fb..1ee400199da 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -93,7 +93,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 +126,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 +135,7 @@ 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 *pg_waldump_path = NULL;
DIR *dir;
@@ -221,8 +221,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 */
@@ -365,15 +365,15 @@ main(int argc, char **argv)
verify_backup_checksums(&context);
/* By default, look for the WAL in the backup directory, too. */
- if (wal_directory == NULL)
- wal_directory = psprintf("%s/pg_wal", context.backup_directory);
+ if (wal_path == NULL)
+ wal_path = psprintf("%s/pg_wal", context.backup_directory);
/*
* 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
@@ -1198,7 +1198,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 +1208,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 +1376,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 a9e24931100..9b5cd5898cf 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 3e3f20c67c5..81442f51c17 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 0cb958f3448..7f729fa35ba 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 da8c72f6427..09937966fa7 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 317b0b71e7f..4da68d0074e 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 c910fb236cc..a948959b54f 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 982751984c7..ef2799316a8 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 acdc3da5e02..eaf91ef1e98 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 64005feedfd..7fb0e5ab1f6 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 17240feeb5c..97125838e8c 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 034b9764232..63f8041ab38 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 b7d97c8976d..fb6fcae8b82 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 c1b710b0a36..568f972b0bb 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/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl
index babc4f0a86b..b07f80719b0 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', '--no-parse-wal', $backup_path ],
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: $!";
--
2.47.1
v2-0009-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchapplication/x-patch; name=v2-0009-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchDownload
From 877dd072349fbfeb4a39f2f3cca13ba4b68d0912 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 17 Jul 2025 16:39:36 +0530
Subject: [PATCH v2 9/9] pg_verifybackup: enabled WAL parsing for tar-format
backup
Now that pg_waldump supports decoding from tar archives, we should
leverage this functionality to remove the previous restriction on WAL
parsing for tar-backed formats.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 5 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 66 +++++++++++++------
src/bin/pg_verifybackup/t/002_algorithm.pl | 4 --
src/bin/pg_verifybackup/t/003_corruption.pl | 4 +-
src/bin/pg_verifybackup/t/008_untar.pl | 3 +-
src/bin/pg_verifybackup/t/010_client_untar.pl | 3 +-
6 files changed, 50 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index e9b8bfd51b1..16b50b5a4df 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -36,10 +36,7 @@ PostgreSQL documentation
<literal>backup_manifest</literal> 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 <application>pg_basebackup</application>. However, at present,
- <literal>WAL</literal> verification is supported only for plain-format
- backups. Therefore, if the backup is stored in tar-format, the
- <literal>-n, --no-parse-wal</literal> option should be used.
+ supported by <application>pg_basebackup</application>.
</para>
<para>
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 1ee400199da..4bfe6fdff16 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);
@@ -136,6 +140,8 @@ main(int argc, char **argv)
bool no_parse_wal = false;
bool quiet = false;
char *wal_path = NULL;
+ char *base_archive_path = NULL;
+ char *wal_archive_path = NULL;
char *pg_waldump_path = NULL;
DIR *dir;
@@ -327,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'.
@@ -346,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.
@@ -364,9 +359,28 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
- /* By default, look for the WAL in the backup directory, too. */
+ /*
+ * 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)
- wal_path = psprintf("%s/pg_wal", context.backup_directory);
+ {
+ 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
@@ -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 <tablespaceoid>.tar file here. */
diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl
index ae16c11bc4d..4f284a9e828 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 1dd60f709cf..f1ebdbb46b4 100644
--- a/src/bin/pg_verifybackup/t/003_corruption.pl
+++ b/src/bin/pg_verifybackup/t/003_corruption.pl
@@ -193,10 +193,8 @@ for my $scenario (@scenario)
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/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl
index bc3d6b352ad..0cfe1f9532c 100644
--- a/src/bin/pg_verifybackup/t/008_untar.pl
+++ b/src/bin/pg_verifybackup/t/008_untar.pl
@@ -123,8 +123,7 @@ for my $tc (@test_configuration)
# 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 b62faeb5acf..76269a73673 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 @@ for my $tc (@test_configuration)
# 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");
--
2.47.1
v2-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchapplication/x-patch; name=v2-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchDownload
From 233cf0977b18100916b0204ad7e57445c420dae6 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 24 Jun 2025 11:33:20 +0530
Subject: [PATCH v2 1/9] Refactor: pg_waldump: Move some declarations to new
pg_waldump.h
This is in preparation for adding a second source file to this
directory.
---
src/bin/pg_waldump/pg_waldump.c | 11 ++---------
src/bin/pg_waldump/pg_waldump.h | 27 +++++++++++++++++++++++++++
2 files changed, 29 insertions(+), 9 deletions(-)
create mode 100644 src/bin/pg_waldump/pg_waldump.h
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 13d3ec2f5be..a49b2fd96c7 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,19 +40,11 @@
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};
-typedef struct XLogDumpPrivate
-{
- TimeLineID timeline;
- XLogRecPtr startptr;
- XLogRecPtr endptr;
- bool endptr_reached;
-} XLogDumpPrivate;
-
typedef struct XLogDumpConfig
{
/* display options */
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
new file mode 100644
index 00000000000..9e62b64ead5
--- /dev/null
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -0,0 +1,27 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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"
+
+extern int WalSegSz;
+
+/* Contains the necessary information to drive WAL decoding */
+typedef struct XLogDumpPrivate
+{
+ TimeLineID timeline;
+ XLogRecPtr startptr;
+ XLogRecPtr endptr;
+ bool endptr_reached;
+} XLogDumpPrivate;
+
+#endif /* end of PG_WALDUMP_H */
--
2.47.1
v2-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchapplication/x-patch; name=v2-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchDownload
From b46d48d7cc9fc42257f3d6da6850ff20f9461ae9 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 26 Jun 2025 11:42:53 +0530
Subject: [PATCH v2 2/9] Refactor: pg_waldump: Separate logic used to calculate
the required read size.
This refactoring prepares the codebase for an upcoming patch that will
support reading WAL from tar files. The logic for calculating the
required read size has been updated to handle both normal WAL files
and WAL files located inside a tar archive.
---
src/bin/pg_waldump/pg_waldump.c | 39 ++++++++++++++++++++++-----------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index a49b2fd96c7..8d0cd9e7156 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -326,6 +326,29 @@ identify_target_directory(char *directory, char *fname)
return NULL; /* not reached */
}
+/* 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,
@@ -383,21 +406,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))
--
2.47.1
v2-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchapplication/x-patch; name=v2-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchDownload
From 8e3236330522fae0674508cebc7d2f1d8379271f Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 30 Jul 2025 12:43:30 +0530
Subject: [PATCH v2 3/9] Refactor: pg_waldump: Restructure TAP tests.
Restructured some tests to run inside a loop, facilitating their
re-execution for decoding WAL from tar archives.
---
src/bin/pg_waldump/t/001_basic.pl | 123 ++++++++++++++++--------------
1 file changed, 67 insertions(+), 56 deletions(-)
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index f26d75e01cf..1b712e8d74d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -198,28 +198,6 @@ command_like(
],
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 +205,6 @@ command_like(
],
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.
@@ -272,7 +241,6 @@ sub test_pg_waldump
my $result = IPC::Run::run [
'pg_waldump',
- '--path' => $node->data_dir,
'--start' => $start_lsn,
'--end' => $end_lsn,
@opts
@@ -288,38 +256,81 @@ sub test_pg_waldump
my @lines;
-@lines = test_pg_waldump;
-is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
+my @scenario = (
+ {
+ 'path' => $node->data_dir
+ });
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenario)
+{
+ my $path = $scenario->{'path'};
-@lines = test_pg_waldump('--fullpage');
-is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
+ SKIP:
+ {
+ 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');
-@lines = test_pg_waldump('--stats');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+ command_fails_like(
+ [
+ 'pg_waldump', '--quiet',
+ '--path' => $path,
+ '--start' => $start_lsn
+ ],
+ qr/error: error in WAL record at/,
+ 'errors are shown with --quiet');
-@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('--path' => $path);
+ is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
-@lines = test_pg_waldump('--rmgr' => 'Btree');
-is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+ @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ is(@lines, 6, 'limit option observed');
-@lines = test_pg_waldump('--fork' => 'init');
-is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+ @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
-@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('--path' => $path, '--stats');
+ like($lines[0], qr/WAL statistics/, "statistics on stdout");
+ is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
-@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');
+ @lines = test_pg_waldump('--path' => $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' => $path, '--rmgr' => 'Btree');
+ is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+
+ @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+
+ @lines = test_pg_waldump('--path' => $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' => $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');
+ }
+}
done_testing();
--
2.47.1
v2-0004-pg_waldump-Rename-directory-creation-routine-for-.patchapplication/x-patch; name=v2-0004-pg_waldump-Rename-directory-creation-routine-for-.patchDownload
From 4baa0189d624cd4606dcab5f5417cf7d305a8223 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 29 Jul 2025 14:59:01 +0530
Subject: [PATCH v2 4/9] pg_waldump: Rename directory creation routine for
generalized use.
The create_fullpage_directory() function, currently used only for
storing full-page images from WAL records, should be renamed to a more
generalized name. This would allow it to be reused in future patches
for creating other directories as needed.
---
src/bin/pg_waldump/pg_waldump.c | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 8d0cd9e7156..4775275c07a 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -114,11 +114,11 @@ verify_directory(const char *directory)
}
/*
- * Create if necessary the directory storing the full-page images extracted
- * from the WAL records read.
+ * Create the directory if it doesn't exist. Report an error if creation fails
+ * or if an existing directory is not empty.
*/
static void
-create_fullpage_directory(char *path)
+create_directory(char *path)
{
int ret;
@@ -1112,8 +1112,12 @@ main(int argc, char **argv)
}
}
+ /*
+ * Create if necessary the directory storing the full-page images
+ * extracted from the WAL records read.
+ */
if (config.save_fullpage_path != NULL)
- create_fullpage_directory(config.save_fullpage_path);
+ create_directory(config.save_fullpage_path);
/* parse files as start/end boundaries, extract path if not specified */
if (optind < argc)
--
2.47.1
v2-0005-pg_waldump-Add-support-for-archived-WAL-decoding.patchapplication/x-patch; name=v2-0005-pg_waldump-Add-support-for-archived-WAL-decoding.patchDownload
From 5f97922e34e7d5a523dbe718a1b61ebb08c7403e Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 18:37:59 +0530
Subject: [PATCH v2 5/9] pg_waldump: Add support for archived WAL decoding.
pg_waldump can now accept the path to a tar archive containing WAL
files and decode them. This feature was added primarily for
pg_verifybackup, which previously disabled WAL parsing for
tar-formatted backups.
Note that this patch requires that the WAL files within the archive be
in sequential order; an error will be reported otherwise. The next
patch is planned to remove this restriction.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/Makefile | 7 +-
src/bin/pg_waldump/astreamer_waldump.c | 378 +++++++++++++++++++++++++
src/bin/pg_waldump/meson.build | 4 +-
src/bin/pg_waldump/pg_waldump.c | 361 +++++++++++++++++++----
src/bin/pg_waldump/pg_waldump.h | 21 +-
src/bin/pg_waldump/t/001_basic.pl | 84 +++++-
src/tools/pgindent/typedefs.list | 1 +
8 files changed, 785 insertions(+), 79 deletions(-)
create mode 100644 src/bin/pg_waldump/astreamer_waldump.c
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index ce23add5577..d004bb0f67e 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -141,13 +141,17 @@ PostgreSQL documentation
<term><option>--path=<replaceable>path</replaceable></option></term>
<listitem>
<para>
- Specifies a directory to search for WAL segment files or a
- directory with a <literal>pg_wal</literal> subdirectory that
+ Specifies a tar archive or a directory to search for WAL segment files
+ or a directory with a <literal>pg_wal</literal> subdirectory that
contains such files. The default is to search in the current
directory, the <literal>pg_wal</literal> subdirectory of the
current directory, and the <literal>pg_wal</literal> subdirectory
of <envar>PGDATA</envar>.
</para>
+ <para>
+ If a tar archive is provided, its WAL segment files must be in
+ sequential order; otherwise, an error will be reported.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile
index 4c1ee649501..b234613eb50 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 00000000000..916d388ef0c
--- /dev/null
+++ b/src/bin/pg_waldump/astreamer_waldump.c
@@ -0,0 +1,378 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 <unistd.h>
+
+#include "access/xlog_internal.h"
+#include "access/xlogdefs.h"
+#include "common/logging.h"
+#include "fe_utils/simple_list.h"
+#include "pg_waldump.h"
+
+/*
+ * How many bytes should we try to read from a file at once?
+ */
+#define READ_CHUNK_SIZE (128 * 1024)
+
+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;
+ XLogSegNo nextSegNo; /* Next expected segment to stream */
+} astreamer_waldump;
+
+static int astreamer_archive_read(XLogDumpPrivate *privateInfo);
+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_member *member,
+ TimeLineID startTimeLineID,
+ XLogSegNo startSegNo,
+ XLogSegNo endSegNo,
+ XLogSegNo nextSegNo,
+ XLogSegNo *curSegNo,
+ TimeLineID *curSegTimeline);
+
+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;
+
+ /*
+ * Ignore existing data if the required target page has not yet been
+ * read.
+ */
+ if (recptr >= endPtr)
+ {
+ len = 0;
+
+ /* Reset the buffer */
+ 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)
+ break; /* No data remaining */
+ }
+ }
+
+ return (count - nbytes) ? (count - nbytes) : -1;
+}
+
+/*
+ * Reads the archive and passes it to the archive streamer for decompression.
+ */
+static 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_content_new(astreamer *next, XLogRecPtr startptr,
+ XLogRecPtr endPtr, XLogDumpPrivate *privateInfo)
+{
+ astreamer_waldump *streamer;
+
+ streamer = palloc0(sizeof(astreamer_waldump));
+ *((const astreamer_ops **) &streamer->base.bbs_ops) =
+ &astreamer_waldump_ops;
+
+ streamer->base.bbs_next = next;
+ initStringInfo(&streamer->base.bbs_buffer);
+
+ if (XLogRecPtrIsInvalid(startptr))
+ streamer->startSegNo = 0;
+ else
+ {
+ XLByteToSeg(startptr, streamer->startSegNo, WalSegSz);
+
+ /*
+ * Initialize the record pointer to the beginning of the first
+ * segment; this pointer will track the WAL record reading status.
+ */
+ XLogSegNoOffsetToRecPtr(streamer->startSegNo, 0, WalSegSz,
+ privateInfo->archive_streamer_read_ptr);
+ }
+
+ 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:
+ {
+ XLogSegNo segNo;
+ TimeLineID timeline;
+
+ pg_log_debug("pg_waldump: reading \"%s\"", member->pathname);
+
+ mystreamer->skipThisSeg = false;
+
+ if (!member_is_relevant_wal(member,
+ privateInfo->timeline,
+ mystreamer->startSegNo,
+ mystreamer->endSegNo,
+ mystreamer->nextSegNo,
+ &segNo, &timeline))
+ {
+ mystreamer->skipThisSeg = true;
+ break;
+ }
+
+ /*
+ * If nextSegNo is 0, the check is skipped, and any WAL file
+ * can be read -- this typically occurs during initial
+ * verification.
+ */
+ if (mystreamer->nextSegNo == 0)
+ break;
+
+ /* WAL segments must be archived in order */
+ if (mystreamer->nextSegNo != segNo)
+ {
+ pg_log_error("WAL files are not archived in sequential order");
+ pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
+ mystreamer->nextSegNo, segNo);
+ exit(1);
+ }
+
+ /*
+ * We track the reading of WAL segment records using a pointer
+ * that's continuously incremented by the length of the
+ * received data. This pointer is crucial for serving WAL page
+ * requests from the WAL decoding routine, so it must be
+ * accurate.
+ */
+#ifdef USE_ASSERT_CHECKING
+ if (mystreamer->nextSegNo != 0)
+ {
+ XLogRecPtr recPtr;
+
+ XLogSegNoOffsetToRecPtr(segNo, 0, WalSegSz, recPtr);
+ Assert(privateInfo->archive_streamer_read_ptr == recPtr);
+ }
+#endif
+
+ /* Save the timeline */
+ privateInfo->timeline = timeline;
+
+ /* Update the next expected segment number */
+ mystreamer->nextSegNo += 1;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_CONTENTS:
+ /* Skip this segment */
+ if (mystreamer->skipThisSeg)
+ break;
+
+ /* Or, copy contents to buffer */
+ privateInfo->archive_streamer_read_ptr += len;
+ astreamer_buffer_bytes(streamer, &data, &len, len);
+ break;
+
+ case ASTREAMER_MEMBER_TRAILER:
+ 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)
+{
+ Assert(streamer->bbs_next == NULL);
+
+ 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_member *member, TimeLineID startTimeLineID,
+ XLogSegNo startSegNo, XLogSegNo endSegNo,
+ XLogSegNo nextSegNo, XLogSegNo *curSegNo,
+ TimeLineID *curSegTimeline)
+{
+ 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);
+
+ /* Ignore the older timeline */
+ if (startTimeLineID > timeline)
+ return false;
+
+ /* Skip if the current segment is not the desired one */
+ if (startSegNo > segNo || endSegNo < segNo)
+ return false;
+
+ *curSegNo = segNo;
+ *curSegTimeline = timeline;
+
+ return true;
+}
diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build
index 937e0d68841..2a0300dc339 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 4775275c07a..64f3a65b735 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -182,10 +182,9 @@ open_file_in_directory(const char *directory, const char *fname)
{
int fd = -1;
char fpath[MAXPGPATH];
+ char *dir = directory ? (char *) directory : ".";
- Assert(directory != NULL);
-
- snprintf(fpath, MAXPGPATH, "%s/%s", directory, fname);
+ snprintf(fpath, MAXPGPATH, "%s/%s", dir, fname);
fd = open(fpath, O_RDONLY | PG_BINARY, 0);
if (fd < 0 && errno != ENOENT)
@@ -326,6 +325,160 @@ 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;
+}
+
+/*
+ * Creates an appropriate chain of archive streamers for reading the given
+ * tar archive.
+ */
+static void
+setup_astreamer(XLogDumpPrivate *private, pg_compress_algorithm compression,
+ XLogRecPtr startptr, XLogRecPtr endptr)
+{
+ astreamer *streamer = NULL;
+
+ streamer = astreamer_waldump_content_new(NULL, 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;
+}
+
+/*
+ * Initializes the archive reader for a tar file.
+ */
+static void
+init_tar_archive_reader(XLogDumpPrivate *private, char *waldir,
+ pg_compress_algorithm compression)
+{
+ int fd;
+
+ /* Now, the 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;
+
+ /* Setup tar archive reading facility */
+ setup_astreamer(private, compression, private->startptr, private->endptr);
+}
+
+/*
+ * 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;
+
+ setup_astreamer(private, compression, InvalidXLogRecPtr, InvalidXLogRecPtr);
+
+ /* Now, the tar archive and store its file descriptor */
+ private->archive_fd = open_file_in_directory(waldir, private->archive_name);
+
+ if (private->archive_fd < 0)
+ pg_fatal("could not open file \"%s\"", private->archive_name);
+
+ /* 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,
@@ -406,7 +559,7 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPagePtr, reqLen);
+ int count = required_read_len(private, targetPtr, reqLen);
WALReadError errinfo;
if (private->endptr_reached)
@@ -436,6 +589,44 @@ 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, targetPtr, reqLen);
+
+ if (private->endptr_reached)
+ return -1;
+
+ /* 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.
@@ -773,8 +964,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"
@@ -806,7 +997,11 @@ main(int argc, char **argv)
XLogRecord *record;
XLogRecPtr first_record;
char *waldir = NULL;
+ char *walpath = NULL;
char *errormsg;
+ bool is_tar = false;
+ XLogReaderRoutine *routine = NULL;
+ pg_compress_algorithm compression;
static struct option long_options[] = {
{"bkp-details", no_argument, NULL, 'b'},
@@ -938,7 +1133,7 @@ main(int argc, char **argv)
}
break;
case 'p':
- waldir = pg_strdup(optarg);
+ walpath = pg_strdup(optarg);
break;
case 'q':
config.quiet = true;
@@ -1102,10 +1297,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;
@@ -1129,44 +1334,23 @@ main(int argc, char **argv)
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;
+ waldir = walpath;
+ 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)
@@ -1174,32 +1358,67 @@ 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)
+ verify_tar_archive(&private, waldir, compression);
/* we don't know what to print */
if (XLogRecPtrIsInvalid(private.startptr))
@@ -1211,11 +1430,26 @@ main(int argc, char **argv)
/* done with argument parsing, do the actual work */
/* we have everything we need, start reading */
+ if (is_tar)
+ {
+ /* Set up for reading tar file */
+ init_tar_archive_reader(&private, waldir, compression);
+
+ /* Routine to decode WAL files in tar archive */
+ routine = XL_ROUTINE(.page_read = TarWALDumpReadPage,
+ .segment_open = TarWALDumpOpenSegment,
+ .segment_close = TarWALDumpCloseSegment);
+ }
+ else
+ {
+ /* Routine to decode WAL files */
+ routine = XL_ROUTINE(.page_read = WALDumpReadPage,
+ .segment_open = WALDumpOpenSegment,
+ .segment_close = WALDumpCloseSegment);
+ }
+
xlogreader_state =
- XLogReaderAllocate(WalSegSz, waldir,
- XL_ROUTINE(.page_read = WALDumpReadPage,
- .segment_open = WALDumpOpenSegment,
- .segment_close = WALDumpCloseSegment),
+ XLogReaderAllocate(WalSegSz, waldir, routine,
&private);
if (!xlogreader_state)
pg_fatal("out of memory while allocating a WAL reading processor");
@@ -1325,6 +1559,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
index 9e62b64ead5..b5d440500de 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -12,6 +12,8 @@
#define PG_WALDUMP_H
#include "access/xlogdefs.h"
+#include "fe_utils/astreamer.h"
+#include "lib/stringinfo.h"
extern int WalSegSz;
@@ -22,6 +24,23 @@ typedef struct XLogDumpPrivate
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 */
} XLogDumpPrivate;
-#endif /* end of PG_WALDUMP_H */
+
+extern astreamer *astreamer_waldump_content_new(astreamer *next,
+ XLogRecPtr startptr,
+ XLogRecPtr endptr,
+ XLogDumpPrivate *privateInfo);
+extern int astreamer_wal_read(char *readBuff, XLogRecPtr startptr, Size count,
+ 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 1b712e8d74d..443126a9ce6 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -3,10 +3,13 @@
use strict;
use warnings FATAL => 'all';
+use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $tar = $ENV{TAR};
+
program_help_ok('pg_waldump');
program_version_ok('pg_waldump');
program_options_handling_ok('pg_waldump');
@@ -235,7 +238,7 @@ command_like(
sub test_pg_waldump
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my @opts = @_;
+ my ($path, @opts) = @_;
my ($stdout, $stderr);
@@ -243,6 +246,7 @@ sub test_pg_waldump
'pg_waldump',
'--start' => $start_lsn,
'--end' => $end_lsn,
+ '--path' => $path,
@opts
],
'>' => \$stdout,
@@ -254,11 +258,50 @@ sub test_pg_waldump
return @lines;
}
-my @lines;
+# 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 = sort @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
+ '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)
@@ -267,6 +310,19 @@ for my $scenario (@scenario)
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/,
@@ -298,38 +354,42 @@ for my $scenario (@scenario)
qr/error: error in WAL record at/,
'errors are shown with --quiet');
- @lines = test_pg_waldump('--path' => $path);
+ my @lines;
+ @lines = test_pg_waldump($path);
is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
- @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ @lines = test_pg_waldump($path, '--limit' => 6);
is(@lines, 6, 'limit option observed');
- @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ @lines = test_pg_waldump($path, '--fullpage');
is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
- @lines = test_pg_waldump('--path' => $path, '--stats');
+ @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' => $path, '--stats=record');
+ @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' => $path, '--rmgr' => 'Btree');
+ @lines = test_pg_waldump($path, '--rmgr' => 'Btree');
is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
- @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ @lines = test_pg_waldump($path, '--fork' => 'init');
is(grep(!/fork init/, @lines), 0, 'only init fork lines');
- @lines = test_pg_waldump('--path' => $path,
+ @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' => $path,
+ @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'};
}
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a13e8162890..b406ca041ec 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3444,6 +3444,7 @@ astreamer_recovery_injector
astreamer_tar_archiver
astreamer_tar_parser
astreamer_verify
+astreamer_waldump
astreamer_zstd_frame
auth_password_hook_typ
autovac_table
--
2.47.1
v2-0006-WIP-pg_waldump-Remove-the-restriction-on-the-orde.patchapplication/x-patch; name=v2-0006-WIP-pg_waldump-Remove-the-restriction-on-the-orde.patchDownload
From dbacb4b3d19c579eb0e3b8aa2dbbff04c7273584 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Mon, 25 Aug 2025 17:26:29 +0530
Subject: [PATCH v2 6/9] WIP-pg_waldump: Remove the restriction on the order of
archived WAL files.
With previous patch, pg_waldump would stop decoding if WAL files were
not in the required sequence. With this patch, decoding will now
continue. Any WAL file that is out of order will be written to a
temporary location, from which it will be read later. Once a temporary
file has been read, it will be removed.
TODO:
Timeline switching is not handled correctly, especially when a
timeline change occurs on the next WAL file that was previously
written to a temporary location.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/astreamer_waldump.c | 189 +++++++++++++++++++++----
src/bin/pg_waldump/pg_waldump.c | 77 +++++++++-
src/bin/pg_waldump/pg_waldump.h | 26 +++-
src/bin/pg_waldump/t/001_basic.pl | 3 +-
5 files changed, 269 insertions(+), 34 deletions(-)
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index d004bb0f67e..8a28b4f0f91 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -149,8 +149,12 @@ PostgreSQL documentation
of <envar>PGDATA</envar>.
</para>
<para>
- If a tar archive is provided, its WAL segment files must be in
- sequential order; otherwise, an error will be reported.
+ If a tar archive is provided and its WAL segment files are not in
+ sequential order, those files will be written to a temporary directory
+ named <filename>pg_waldump_tmp_dir/</filename>. This directory will be
+ created inside the directory specified by the <envar>TMPDIR</envar>
+ environment variable if it is set; otherwise, it will be created within
+ the same directory as the tar archive.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/astreamer_waldump.c b/src/bin/pg_waldump/astreamer_waldump.c
index 916d388ef0c..acfbace7502 100644
--- a/src/bin/pg_waldump/astreamer_waldump.c
+++ b/src/bin/pg_waldump/astreamer_waldump.c
@@ -18,8 +18,8 @@
#include "access/xlog_internal.h"
#include "access/xlogdefs.h"
+#include "common/file_perm.h"
#include "common/logging.h"
-#include "fe_utils/simple_list.h"
#include "pg_waldump.h"
/*
@@ -37,6 +37,8 @@ typedef struct astreamer_waldump
/* These fields change with archive member. */
bool skipThisSeg;
+ bool writeThisSeg;
+ FILE *segFp;
XLogSegNo nextSegNo; /* Next expected segment to stream */
} astreamer_waldump;
@@ -53,8 +55,15 @@ static bool member_is_relevant_wal(astreamer_member *member,
XLogSegNo startSegNo,
XLogSegNo endSegNo,
XLogSegNo nextSegNo,
+ char **curFname,
XLogSegNo *curSegNo,
TimeLineID *curSegTimeline);
+static FILE *member_prepare_tmp_write(XLogSegNo curSegNo,
+ const char *fname,
+ XLogDumpPrivate *privateInfo);
+static XLogSegNo member_next_segno(XLogSegNo curSegNo,
+ TimeLineID timeline,
+ XLogDumpPrivate *privateInfo);
static const astreamer_ops astreamer_waldump_ops = {
.content = astreamer_waldump_content,
@@ -189,17 +198,8 @@ astreamer_waldump_content_new(astreamer *next, XLogRecPtr startptr,
if (XLogRecPtrIsInvalid(startptr))
streamer->startSegNo = 0;
else
- {
XLByteToSeg(startptr, streamer->startSegNo, WalSegSz);
- /*
- * Initialize the record pointer to the beginning of the first
- * segment; this pointer will track the WAL record reading status.
- */
- XLogSegNoOffsetToRecPtr(streamer->startSegNo, 0, WalSegSz,
- privateInfo->archive_streamer_read_ptr);
- }
-
if (XLogRecPtrIsInvalid(endPtr))
streamer->endSegNo = UINT64_MAX;
else
@@ -228,19 +228,21 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
{
case ASTREAMER_MEMBER_HEADER:
{
+ char *fname;
XLogSegNo segNo;
TimeLineID timeline;
pg_log_debug("pg_waldump: reading \"%s\"", member->pathname);
mystreamer->skipThisSeg = false;
+ mystreamer->writeThisSeg = false;
if (!member_is_relevant_wal(member,
privateInfo->timeline,
mystreamer->startSegNo,
mystreamer->endSegNo,
mystreamer->nextSegNo,
- &segNo, &timeline))
+ &fname, &segNo, &timeline))
{
mystreamer->skipThisSeg = true;
break;
@@ -254,24 +256,38 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
if (mystreamer->nextSegNo == 0)
break;
- /* WAL segments must be archived in order */
+ /*
+ * 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 != segNo)
{
- pg_log_error("WAL files are not archived in sequential order");
- pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
- mystreamer->nextSegNo, segNo);
- exit(1);
+ mystreamer->writeThisSeg = true;
+ mystreamer->segFp =
+ member_prepare_tmp_write(segNo, fname, privateInfo);
+ break;
}
/*
- * We track the reading of WAL segment records using a pointer
- * that's continuously incremented by the length of the
- * received data. This pointer is crucial for serving WAL page
- * requests from the WAL decoding routine, so it must be
- * accurate.
+ * 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.
+ * Also, if there's any existing data in the buffer, the next
+ * WAL record should logically follow it.
*/
#ifdef USE_ASSERT_CHECKING
- if (mystreamer->nextSegNo != 0)
+ Assert(!mystreamer->skipThisSeg);
+ Assert(!mystreamer->writeThisSeg);
+
+ if (privateInfo->archive_streamer_buf->len != 0)
{
XLogRecPtr recPtr;
@@ -280,11 +296,19 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
}
#endif
+ /*
+ * Initialized to the beginning of the current segment being
+ * streamed through the buffer.
+ */
+ XLogSegNoOffsetToRecPtr(segNo, 0, WalSegSz,
+ privateInfo->archive_streamer_read_ptr);
+
/* Save the timeline */
privateInfo->timeline = timeline;
/* Update the next expected segment number */
- mystreamer->nextSegNo += 1;
+ mystreamer->nextSegNo =
+ member_next_segno(segNo, timeline, privateInfo);
}
break;
@@ -293,12 +317,44 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
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/%s\": %m",
+ privateInfo->tmpdir, 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;
+ }
break;
case ASTREAMER_ARCHIVE_TRAILER:
@@ -325,8 +381,14 @@ astreamer_waldump_finalize(astreamer *streamer)
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);
}
@@ -339,8 +401,8 @@ astreamer_waldump_free(astreamer *streamer)
static bool
member_is_relevant_wal(astreamer_member *member, TimeLineID startTimeLineID,
XLogSegNo startSegNo, XLogSegNo endSegNo,
- XLogSegNo nextSegNo, XLogSegNo *curSegNo,
- TimeLineID *curSegTimeline)
+ XLogSegNo nextSegNo, char **curFname,
+ XLogSegNo *curSegNo, TimeLineID *curSegTimeline)
{
int pathlen;
XLogSegNo segNo;
@@ -371,8 +433,85 @@ member_is_relevant_wal(astreamer_member *member, TimeLineID startTimeLineID,
if (startSegNo > segNo || endSegNo < segNo)
return false;
+ *curFname = fname;
*curSegNo = segNo;
*curSegTimeline = timeline;
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,
+ XLogDumpPrivate *privateInfo)
+{
+ FILE *file;
+ char *fpath = get_tmp_wal_file_path(privateInfo, 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
+
+ /* Record this segment's export */
+ simple_string_list_append(&privateInfo->exportedSegList, 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,
+ XLogDumpPrivate *privateInfo)
+{
+ 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(&privateInfo->exportedSegList, fname))
+ {
+ nextSegNo += 1;
+ exists = true;
+ }
+ } while (exists);
+
+ return nextSegNo;
+}
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 64f3a65b735..d456adce59c 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -393,13 +393,14 @@ setup_astreamer(XLogDumpPrivate *private, pg_compress_algorithm compression,
}
/*
- * Initializes the archive reader for a tar file.
+ * Initializes the tar archive reader and a temporary directory for WAL files.
*/
static void
init_tar_archive_reader(XLogDumpPrivate *private, char *waldir,
pg_compress_algorithm compression)
{
int fd;
+ char *tmpdir;
/* Now, the tar archive and store its file descriptor */
fd = open_file_in_directory(waldir, private->archive_name);
@@ -411,6 +412,15 @@ init_tar_archive_reader(XLogDumpPrivate *private, char *waldir,
/* Setup tar archive reading facility */
setup_astreamer(private, compression, private->startptr, private->endptr);
+
+ /* Temporary space for writing WAL segments */
+ if (getenv("TMPDIR"))
+ tmpdir = pstrdup(getenv("TMPDIR"));
+ else
+ tmpdir = waldir != NULL ? pstrdup(waldir) : pstrdup(".");
+ canonicalize_path(tmpdir);
+
+ private->tmpdir = tmpdir;
}
/*
@@ -419,6 +429,8 @@ init_tar_archive_reader(XLogDumpPrivate *private, char *waldir,
static void
free_tar_archive_reader(XLogDumpPrivate *private)
{
+ SimpleStringListCell *cell;
+
/*
* NB: Normally, astreamer_finalize() is called before astreamer_free() to
* flush any remaining buffered data or to ensure the end of the tar
@@ -432,6 +444,15 @@ free_tar_archive_reader(XLogDumpPrivate *private)
if (close(private->archive_fd) != 0)
pg_log_error("could not close file \"%s\": %m",
private->archive_name);
+
+ /* Clear out any existing temporary files */
+ for (cell = private->exportedSegList.head; cell; cell = cell->next)
+ {
+ char *fpath = get_tmp_wal_file_path(private, cell->val);
+
+ unlink(fpath);
+ pfree(fpath);
+ }
}
/*
@@ -559,7 +580,7 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPtr, reqLen);
+ int count = required_read_len(private, targetPagePtr, reqLen);
WALReadError errinfo;
if (private->endptr_reached)
@@ -618,12 +639,60 @@ TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPtr, reqLen);
+ int count = required_read_len(private, targetPagePtr, reqLen);
+ XLogSegNo nextSegNo;
if (private->endptr_reached)
return -1;
- /* Read the WAL page from the archive streamer */
+ /*
+ * If the target page is in a different segment, first check for the WAL
+ * segment's physical existence in the temporary directory.
+ *
+ * XXX: Timeline change is not handled.
+ */
+ nextSegNo = state->seg.ws_segno;
+ if (!XLByteInSeg(targetPagePtr, nextSegNo, WalSegSz))
+ {
+ char fname[MAXPGPATH];
+ 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(private, fname);
+ 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(&private->exportedSegList, fname))
+ {
+ fpath = get_tmp_wal_file_path(private, 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);
+ }
+ }
+
+ /* Continue reading from the open WAL segment, if any */
+ if (state->seg.ws_file >= 0)
+ return WALDumpReadPage(state, targetPagePtr, reqLen, targetPtr,
+ readBuff);
+
+ /* Otherwise, read the WAL page from the archive streamer */
return astreamer_wal_read(readBuff, targetPagePtr, count, private);
}
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
index b5d440500de..614e679cb96 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -13,8 +13,11 @@
#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;
/* Contains the necessary information to drive WAL decoding */
@@ -31,11 +34,30 @@ typedef struct XLogDumpPrivate
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 */
+ XLogRecPtr archive_streamer_read_ptr; /* Populate the buffer with
+ * records until this record
+ * pointer */
+ char *tmpdir; /* Temporary direcotry to export file */
+ SimpleStringList exportedSegList; /* Temporary exported WAL file list */
} XLogDumpPrivate;
+/*
+ * Generate the temporary WAL file path.
+ *
+ * Note that the caller is responsible to pfree it.
+ */
+static inline char *
+get_tmp_wal_file_path(XLogDumpPrivate *privateInfo, const char *fname)
+{
+ char *fpath = (char *) palloc(MAXPGPATH);
+
+ snprintf(fpath, MAXPGPATH, "%s/%s.%s", privateInfo->tmpdir, fname,
+ TEMP_FILE_EXT);
+
+ return fpath;
+}
+
extern astreamer *astreamer_waldump_content_new(astreamer *next,
XLogRecPtr startptr,
XLogRecPtr endptr,
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 443126a9ce6..d5fa1f6d28d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -7,6 +7,7 @@ use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+use List::Util qw(shuffle);
my $tar = $ENV{TAR};
@@ -272,7 +273,7 @@ sub generate_archive
}
closedir $dh;
- @files = sort @files;
+ @files = shuffle @files;
# move into the WAL directory before archiving files
my $cwd = getcwd;
--
2.47.1
v2-0007-pg_verifybackup-Delay-default-WAL-directory-prepa.patchapplication/x-patch; name=v2-0007-pg_verifybackup-Delay-default-WAL-directory-prepa.patchDownload
From 64dbdfaa575749b76ebdd3fd235a8186b6eb19fc Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 14:47:43 +0530
Subject: [PATCH v2 7/9] pg_verifybackup: Delay default WAL directory
preparation.
We are not sure whether to parse WAL from a directory or an archive
until the backup format is known. Therefore, we delay preparing the
default WAL directory until the point of parsing. This delay is
harmless, as the WAL directory is not used elsewhere.
---
src/bin/pg_verifybackup/pg_verifybackup.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 5e6c13bb921..31ebc1581fb 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -285,10 +285,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
@@ -368,6 +364,10 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
+ /* 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 parse the required ranges of WAL records, unless we were told
* not to do so.
--
2.47.1
On Mon, Aug 25, 2025 at 5:58 PM Amul Sul <sulamul@gmail.com> wrote:
On Thu, Aug 7, 2025 at 7:47 PM Amul Sul <sulamul@gmail.com> wrote:
[....]
-----------------------------------
Known Issues & Status:
-----------------------------------
- Timeline Switching: The current implementation in patch 006 does not
correctly handle timeline switching. This is a known issue, especially
when a timeline change occurs on a WAL file that has been written to a
temporary location.This is still pending and will be addressed in the next version.
Therefore, patch 0006 remains marked as WIP.
After testing pg_waldump, I have realised that my previous
understanding of its timeline handling was incorrect. I had mistakenly
assumed by reading xlogreader code that it would use the same
timeline-switching logic found in xlogreader, without first verifying
this behavior. In testing, I found that pg_waldump does not follow
timeline switches. Instead, it expects all WAL files to be from a
single timeline, which is either specified by the user or determined
from the starting segment or default 1.
This is a positive finding, as it means we don't need to make
significant changes to align pg_waldump's current behavior. The
attached patches are now complete and no longer works in progress --
read for review. Additionally, I've dropped patch v2-0004 because it is
no longer necessary. The primary patches that implement the proposed
feature are now 0004 and 0005 in the attached set.
Regards,
Amul
Attachments:
v3-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchapplication/x-patch; name=v3-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchDownload
From b48d5a7ed121c694273ad8cf2c3c78aa4ae23b1d Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 24 Jun 2025 11:33:20 +0530
Subject: [PATCH v3 1/8] Refactor: pg_waldump: Move some declarations to new
pg_waldump.h
This is in preparation for adding a second source file to this
directory.
---
src/bin/pg_waldump/pg_waldump.c | 11 ++---------
src/bin/pg_waldump/pg_waldump.h | 27 +++++++++++++++++++++++++++
2 files changed, 29 insertions(+), 9 deletions(-)
create mode 100644 src/bin/pg_waldump/pg_waldump.h
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 13d3ec2f5be..a49b2fd96c7 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,19 +40,11 @@
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};
-typedef struct XLogDumpPrivate
-{
- TimeLineID timeline;
- XLogRecPtr startptr;
- XLogRecPtr endptr;
- bool endptr_reached;
-} XLogDumpPrivate;
-
typedef struct XLogDumpConfig
{
/* display options */
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
new file mode 100644
index 00000000000..9e62b64ead5
--- /dev/null
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -0,0 +1,27 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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"
+
+extern int WalSegSz;
+
+/* Contains the necessary information to drive WAL decoding */
+typedef struct XLogDumpPrivate
+{
+ TimeLineID timeline;
+ XLogRecPtr startptr;
+ XLogRecPtr endptr;
+ bool endptr_reached;
+} XLogDumpPrivate;
+
+#endif /* end of PG_WALDUMP_H */
--
2.47.1
v3-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchapplication/x-patch; name=v3-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchDownload
From c42fad18faa0016eee4e2eee2e4d0d465156a787 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 26 Jun 2025 11:42:53 +0530
Subject: [PATCH v3 2/8] Refactor: pg_waldump: Separate logic used to calculate
the required read size.
This refactoring prepares the codebase for an upcoming patch that will
support reading WAL from tar files. The logic for calculating the
required read size has been updated to handle both normal WAL files
and WAL files located inside a tar archive.
---
src/bin/pg_waldump/pg_waldump.c | 39 ++++++++++++++++++++++-----------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index a49b2fd96c7..8d0cd9e7156 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -326,6 +326,29 @@ identify_target_directory(char *directory, char *fname)
return NULL; /* not reached */
}
+/* 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,
@@ -383,21 +406,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))
--
2.47.1
v3-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchapplication/x-patch; name=v3-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchDownload
From 31c4a7d8d6a24892e5c8bb476ea5665e15d93aec Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 30 Jul 2025 12:43:30 +0530
Subject: [PATCH v3 3/8] Refactor: pg_waldump: Restructure TAP tests.
Restructured some tests to run inside a loop, facilitating their
re-execution for decoding WAL from tar archives.
---
src/bin/pg_waldump/t/001_basic.pl | 123 ++++++++++++++++--------------
1 file changed, 67 insertions(+), 56 deletions(-)
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index f26d75e01cf..1b712e8d74d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -198,28 +198,6 @@ command_like(
],
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 +205,6 @@ command_like(
],
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.
@@ -272,7 +241,6 @@ sub test_pg_waldump
my $result = IPC::Run::run [
'pg_waldump',
- '--path' => $node->data_dir,
'--start' => $start_lsn,
'--end' => $end_lsn,
@opts
@@ -288,38 +256,81 @@ sub test_pg_waldump
my @lines;
-@lines = test_pg_waldump;
-is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
+my @scenario = (
+ {
+ 'path' => $node->data_dir
+ });
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenario)
+{
+ my $path = $scenario->{'path'};
-@lines = test_pg_waldump('--fullpage');
-is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
+ SKIP:
+ {
+ 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');
-@lines = test_pg_waldump('--stats');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+ command_fails_like(
+ [
+ 'pg_waldump', '--quiet',
+ '--path' => $path,
+ '--start' => $start_lsn
+ ],
+ qr/error: error in WAL record at/,
+ 'errors are shown with --quiet');
-@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('--path' => $path);
+ is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
-@lines = test_pg_waldump('--rmgr' => 'Btree');
-is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+ @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ is(@lines, 6, 'limit option observed');
-@lines = test_pg_waldump('--fork' => 'init');
-is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+ @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
-@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('--path' => $path, '--stats');
+ like($lines[0], qr/WAL statistics/, "statistics on stdout");
+ is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
-@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');
+ @lines = test_pg_waldump('--path' => $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' => $path, '--rmgr' => 'Btree');
+ is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+
+ @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+
+ @lines = test_pg_waldump('--path' => $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' => $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');
+ }
+}
done_testing();
--
2.47.1
v3-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchapplication/x-patch; name=v3-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchDownload
From f15b7e6d33e107cb141586af6072b41266eff0eb Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 18:37:59 +0530
Subject: [PATCH v3 4/8] pg_waldump: Add support for archived WAL decoding.
pg_waldump can now accept the path to a tar archive containing WAL
files and decode them. This feature was added primarily for
pg_verifybackup, which previously disabled WAL parsing for
tar-formatted backups.
Note that this patch requires that the WAL files within the archive be
in sequential order; an error will be reported otherwise. The next
patch is planned to remove this restriction.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/Makefile | 7 +-
src/bin/pg_waldump/astreamer_waldump.c | 378 +++++++++++++++++++++++++
src/bin/pg_waldump/meson.build | 4 +-
src/bin/pg_waldump/pg_waldump.c | 362 +++++++++++++++++++----
src/bin/pg_waldump/pg_waldump.h | 21 +-
src/bin/pg_waldump/t/001_basic.pl | 84 +++++-
src/tools/pgindent/typedefs.list | 1 +
8 files changed, 787 insertions(+), 78 deletions(-)
create mode 100644 src/bin/pg_waldump/astreamer_waldump.c
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index ce23add5577..d004bb0f67e 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -141,13 +141,17 @@ PostgreSQL documentation
<term><option>--path=<replaceable>path</replaceable></option></term>
<listitem>
<para>
- Specifies a directory to search for WAL segment files or a
- directory with a <literal>pg_wal</literal> subdirectory that
+ Specifies a tar archive or a directory to search for WAL segment files
+ or a directory with a <literal>pg_wal</literal> subdirectory that
contains such files. The default is to search in the current
directory, the <literal>pg_wal</literal> subdirectory of the
current directory, and the <literal>pg_wal</literal> subdirectory
of <envar>PGDATA</envar>.
</para>
+ <para>
+ If a tar archive is provided, its WAL segment files must be in
+ sequential order; otherwise, an error will be reported.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile
index 4c1ee649501..b234613eb50 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 00000000000..61876e834a9
--- /dev/null
+++ b/src/bin/pg_waldump/astreamer_waldump.c
@@ -0,0 +1,378 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 <unistd.h>
+
+#include "access/xlog_internal.h"
+#include "access/xlogdefs.h"
+#include "common/logging.h"
+#include "fe_utils/simple_list.h"
+#include "pg_waldump.h"
+
+/*
+ * How many bytes should we try to read from a file at once?
+ */
+#define READ_CHUNK_SIZE (128 * 1024)
+
+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;
+ XLogSegNo nextSegNo; /* Next expected segment to stream */
+} astreamer_waldump;
+
+static int astreamer_archive_read(XLogDumpPrivate *privateInfo);
+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_member *member,
+ TimeLineID startTimeLineID,
+ XLogSegNo startSegNo,
+ XLogSegNo endSegNo,
+ XLogSegNo nextSegNo,
+ XLogSegNo *curSegNo,
+ TimeLineID *curSegTimeline);
+
+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;
+
+ /*
+ * Ignore existing data if the required target page has not yet been
+ * read.
+ */
+ if (recptr >= endPtr)
+ {
+ len = 0;
+
+ /* Reset the buffer */
+ 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)
+ break; /* No data remaining */
+ }
+ }
+
+ return (count - nbytes) ? (count - nbytes) : -1;
+}
+
+/*
+ * Reads the archive and passes it to the archive streamer for decompression.
+ */
+static 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_content_new(astreamer *next, XLogRecPtr startptr,
+ XLogRecPtr endPtr, XLogDumpPrivate *privateInfo)
+{
+ astreamer_waldump *streamer;
+
+ streamer = palloc0(sizeof(astreamer_waldump));
+ *((const astreamer_ops **) &streamer->base.bbs_ops) =
+ &astreamer_waldump_ops;
+
+ streamer->base.bbs_next = next;
+ initStringInfo(&streamer->base.bbs_buffer);
+
+ if (XLogRecPtrIsInvalid(startptr))
+ streamer->startSegNo = 0;
+ else
+ {
+ XLByteToSeg(startptr, streamer->startSegNo, WalSegSz);
+
+ /*
+ * Initialize the record pointer to the beginning of the first
+ * segment; this pointer will track the WAL record reading status.
+ */
+ XLogSegNoOffsetToRecPtr(streamer->startSegNo, 0, WalSegSz,
+ privateInfo->archive_streamer_read_ptr);
+ }
+
+ 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:
+ {
+ XLogSegNo segNo;
+ TimeLineID timeline;
+
+ pg_log_debug("pg_waldump: reading \"%s\"", member->pathname);
+
+ mystreamer->skipThisSeg = false;
+
+ if (!member_is_relevant_wal(member,
+ privateInfo->timeline,
+ mystreamer->startSegNo,
+ mystreamer->endSegNo,
+ mystreamer->nextSegNo,
+ &segNo, &timeline))
+ {
+ mystreamer->skipThisSeg = true;
+ break;
+ }
+
+ /*
+ * If nextSegNo is 0, the check is skipped, and any WAL file
+ * can be read -- this typically occurs during initial
+ * verification.
+ */
+ if (mystreamer->nextSegNo == 0)
+ break;
+
+ /* WAL segments must be archived in order */
+ if (mystreamer->nextSegNo != segNo)
+ {
+ pg_log_error("WAL files are not archived in sequential order");
+ pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
+ mystreamer->nextSegNo, segNo);
+ exit(1);
+ }
+
+ /*
+ * We track the reading of WAL segment records using a pointer
+ * that's continuously incremented by the length of the
+ * received data. This pointer is crucial for serving WAL page
+ * requests from the WAL decoding routine, so it must be
+ * accurate.
+ */
+#ifdef USE_ASSERT_CHECKING
+ if (mystreamer->nextSegNo != 0)
+ {
+ XLogRecPtr recPtr;
+
+ XLogSegNoOffsetToRecPtr(segNo, 0, WalSegSz, recPtr);
+ Assert(privateInfo->archive_streamer_read_ptr == recPtr);
+ }
+#endif
+
+ /* Save the timeline */
+ privateInfo->timeline = timeline;
+
+ /* Update the next expected segment number */
+ mystreamer->nextSegNo += 1;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_CONTENTS:
+ /* Skip this segment */
+ if (mystreamer->skipThisSeg)
+ break;
+
+ /* Or, copy contents to buffer */
+ privateInfo->archive_streamer_read_ptr += len;
+ astreamer_buffer_bytes(streamer, &data, &len, len);
+ break;
+
+ case ASTREAMER_MEMBER_TRAILER:
+ 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)
+{
+ Assert(streamer->bbs_next == NULL);
+
+ 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_member *member, TimeLineID startTimeLineID,
+ XLogSegNo startSegNo, XLogSegNo endSegNo,
+ XLogSegNo nextSegNo, XLogSegNo *curSegNo,
+ TimeLineID *curSegTimeline)
+{
+ 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);
+
+ /* Ignore if the timeline is different */
+ if (startTimeLineID != timeline)
+ return false;
+
+ /* Skip if the current segment is not the desired one */
+ if (startSegNo > segNo || endSegNo < segNo)
+ return false;
+
+ *curSegNo = segNo;
+ *curSegTimeline = timeline;
+
+ return true;
+}
diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build
index 937e0d68841..2a0300dc339 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 8d0cd9e7156..d136f8f038e 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -326,6 +326,160 @@ 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;
+}
+
+/*
+ * Creates an appropriate chain of archive streamers for reading the given
+ * tar archive.
+ */
+static void
+setup_astreamer(XLogDumpPrivate *private, pg_compress_algorithm compression,
+ XLogRecPtr startptr, XLogRecPtr endptr)
+{
+ astreamer *streamer = NULL;
+
+ streamer = astreamer_waldump_content_new(NULL, 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;
+}
+
+/*
+ * Initializes the archive reader for a tar file.
+ */
+static void
+init_tar_archive_reader(XLogDumpPrivate *private, char *waldir,
+ pg_compress_algorithm compression)
+{
+ int fd;
+
+ /* Now, the 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;
+
+ /* Setup tar archive reading facility */
+ setup_astreamer(private, compression, private->startptr, private->endptr);
+}
+
+/*
+ * 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;
+
+ setup_astreamer(private, compression, InvalidXLogRecPtr, InvalidXLogRecPtr);
+
+ /* Now, the tar archive and store its file descriptor */
+ private->archive_fd = open_file_in_directory(waldir, private->archive_name);
+
+ if (private->archive_fd < 0)
+ pg_fatal("could not open file \"%s\"", private->archive_name);
+
+ /* 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,
@@ -406,7 +560,7 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPagePtr, reqLen);
+ int count = required_read_len(private, targetPtr, reqLen);
WALReadError errinfo;
if (private->endptr_reached)
@@ -436,6 +590,44 @@ 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, targetPtr, reqLen);
+
+ if (private->endptr_reached)
+ return -1;
+
+ /* 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.
@@ -773,8 +965,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"
@@ -806,7 +998,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'},
@@ -938,7 +1133,7 @@ main(int argc, char **argv)
}
break;
case 'p':
- waldir = pg_strdup(optarg);
+ walpath = pg_strdup(optarg);
break;
case 'q':
config.quiet = true;
@@ -1102,10 +1297,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;
@@ -1125,44 +1330,23 @@ main(int argc, char **argv)
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;
+ waldir = walpath ? pg_strdup(walpath) : pg_strdup(".");
+ 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)
@@ -1170,32 +1354,67 @@ 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)
+ verify_tar_archive(&private, waldir, compression);
/* we don't know what to print */
if (XLogRecPtrIsInvalid(private.startptr))
@@ -1207,12 +1426,30 @@ 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, compression);
+
+ /* 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");
@@ -1321,6 +1558,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
index 9e62b64ead5..b5d440500de 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -12,6 +12,8 @@
#define PG_WALDUMP_H
#include "access/xlogdefs.h"
+#include "fe_utils/astreamer.h"
+#include "lib/stringinfo.h"
extern int WalSegSz;
@@ -22,6 +24,23 @@ typedef struct XLogDumpPrivate
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 */
} XLogDumpPrivate;
-#endif /* end of PG_WALDUMP_H */
+
+extern astreamer *astreamer_waldump_content_new(astreamer *next,
+ XLogRecPtr startptr,
+ XLogRecPtr endptr,
+ XLogDumpPrivate *privateInfo);
+extern int astreamer_wal_read(char *readBuff, XLogRecPtr startptr, Size count,
+ 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 1b712e8d74d..443126a9ce6 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -3,10 +3,13 @@
use strict;
use warnings FATAL => 'all';
+use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $tar = $ENV{TAR};
+
program_help_ok('pg_waldump');
program_version_ok('pg_waldump');
program_options_handling_ok('pg_waldump');
@@ -235,7 +238,7 @@ command_like(
sub test_pg_waldump
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my @opts = @_;
+ my ($path, @opts) = @_;
my ($stdout, $stderr);
@@ -243,6 +246,7 @@ sub test_pg_waldump
'pg_waldump',
'--start' => $start_lsn,
'--end' => $end_lsn,
+ '--path' => $path,
@opts
],
'>' => \$stdout,
@@ -254,11 +258,50 @@ sub test_pg_waldump
return @lines;
}
-my @lines;
+# 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 = sort @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
+ '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)
@@ -267,6 +310,19 @@ for my $scenario (@scenario)
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/,
@@ -298,38 +354,42 @@ for my $scenario (@scenario)
qr/error: error in WAL record at/,
'errors are shown with --quiet');
- @lines = test_pg_waldump('--path' => $path);
+ my @lines;
+ @lines = test_pg_waldump($path);
is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
- @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ @lines = test_pg_waldump($path, '--limit' => 6);
is(@lines, 6, 'limit option observed');
- @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ @lines = test_pg_waldump($path, '--fullpage');
is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
- @lines = test_pg_waldump('--path' => $path, '--stats');
+ @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' => $path, '--stats=record');
+ @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' => $path, '--rmgr' => 'Btree');
+ @lines = test_pg_waldump($path, '--rmgr' => 'Btree');
is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
- @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ @lines = test_pg_waldump($path, '--fork' => 'init');
is(grep(!/fork init/, @lines), 0, 'only init fork lines');
- @lines = test_pg_waldump('--path' => $path,
+ @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' => $path,
+ @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'};
}
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a13e8162890..b406ca041ec 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3444,6 +3444,7 @@ astreamer_recovery_injector
astreamer_tar_archiver
astreamer_tar_parser
astreamer_verify
+astreamer_waldump
astreamer_zstd_frame
auth_password_hook_typ
autovac_table
--
2.47.1
v3-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchapplication/x-patch; name=v3-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchDownload
From cf0201067a8627049838e85eecbe5da9aa9c8ef0 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Mon, 25 Aug 2025 17:26:29 +0530
Subject: [PATCH v3 5/8] pg_waldump: Remove the restriction on the order of
archived WAL files.
With previous patch, pg_waldump would stop decoding if WAL files were
not in the required sequence. With this patch, decoding will now
continue. Any WAL file that is out of order will be written to a
temporary location, from which it will be read later. Once a temporary
file has been read, it will be removed.
---
doc/src/sgml/ref/pg_waldump.sgml | 7 +-
src/bin/pg_waldump/astreamer_waldump.c | 189 +++++++++++++++++++++----
src/bin/pg_waldump/pg_waldump.c | 75 +++++++++-
src/bin/pg_waldump/pg_waldump.h | 26 +++-
src/bin/pg_waldump/t/001_basic.pl | 3 +-
5 files changed, 266 insertions(+), 34 deletions(-)
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index d004bb0f67e..c1afb4097b5 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -149,8 +149,11 @@ PostgreSQL documentation
of <envar>PGDATA</envar>.
</para>
<para>
- If a tar archive is provided, its WAL segment files must be in
- sequential order; otherwise, an error will be reported.
+ 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 <envar>TMPDIR</envar>
+ environment variable if it is set; otherwise, the temporary files will
+ be created within the same directory as the tar archive itself.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/astreamer_waldump.c b/src/bin/pg_waldump/astreamer_waldump.c
index 61876e834a9..183f389d3f1 100644
--- a/src/bin/pg_waldump/astreamer_waldump.c
+++ b/src/bin/pg_waldump/astreamer_waldump.c
@@ -18,8 +18,8 @@
#include "access/xlog_internal.h"
#include "access/xlogdefs.h"
+#include "common/file_perm.h"
#include "common/logging.h"
-#include "fe_utils/simple_list.h"
#include "pg_waldump.h"
/*
@@ -37,6 +37,8 @@ typedef struct astreamer_waldump
/* These fields change with archive member. */
bool skipThisSeg;
+ bool writeThisSeg;
+ FILE *segFp;
XLogSegNo nextSegNo; /* Next expected segment to stream */
} astreamer_waldump;
@@ -53,8 +55,15 @@ static bool member_is_relevant_wal(astreamer_member *member,
XLogSegNo startSegNo,
XLogSegNo endSegNo,
XLogSegNo nextSegNo,
+ char **curFname,
XLogSegNo *curSegNo,
TimeLineID *curSegTimeline);
+static FILE *member_prepare_tmp_write(XLogSegNo curSegNo,
+ const char *fname,
+ XLogDumpPrivate *privateInfo);
+static XLogSegNo member_next_segno(XLogSegNo curSegNo,
+ TimeLineID timeline,
+ XLogDumpPrivate *privateInfo);
static const astreamer_ops astreamer_waldump_ops = {
.content = astreamer_waldump_content,
@@ -189,17 +198,8 @@ astreamer_waldump_content_new(astreamer *next, XLogRecPtr startptr,
if (XLogRecPtrIsInvalid(startptr))
streamer->startSegNo = 0;
else
- {
XLByteToSeg(startptr, streamer->startSegNo, WalSegSz);
- /*
- * Initialize the record pointer to the beginning of the first
- * segment; this pointer will track the WAL record reading status.
- */
- XLogSegNoOffsetToRecPtr(streamer->startSegNo, 0, WalSegSz,
- privateInfo->archive_streamer_read_ptr);
- }
-
if (XLogRecPtrIsInvalid(endPtr))
streamer->endSegNo = UINT64_MAX;
else
@@ -228,19 +228,21 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
{
case ASTREAMER_MEMBER_HEADER:
{
+ char *fname;
XLogSegNo segNo;
TimeLineID timeline;
pg_log_debug("pg_waldump: reading \"%s\"", member->pathname);
mystreamer->skipThisSeg = false;
+ mystreamer->writeThisSeg = false;
if (!member_is_relevant_wal(member,
privateInfo->timeline,
mystreamer->startSegNo,
mystreamer->endSegNo,
mystreamer->nextSegNo,
- &segNo, &timeline))
+ &fname, &segNo, &timeline))
{
mystreamer->skipThisSeg = true;
break;
@@ -254,24 +256,38 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
if (mystreamer->nextSegNo == 0)
break;
- /* WAL segments must be archived in order */
+ /*
+ * 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 != segNo)
{
- pg_log_error("WAL files are not archived in sequential order");
- pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
- mystreamer->nextSegNo, segNo);
- exit(1);
+ mystreamer->writeThisSeg = true;
+ mystreamer->segFp =
+ member_prepare_tmp_write(segNo, fname, privateInfo);
+ break;
}
/*
- * We track the reading of WAL segment records using a pointer
- * that's continuously incremented by the length of the
- * received data. This pointer is crucial for serving WAL page
- * requests from the WAL decoding routine, so it must be
- * accurate.
+ * 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.
+ * Also, if there's any existing data in the buffer, the next
+ * WAL record should logically follow it.
*/
#ifdef USE_ASSERT_CHECKING
- if (mystreamer->nextSegNo != 0)
+ Assert(!mystreamer->skipThisSeg);
+ Assert(!mystreamer->writeThisSeg);
+
+ if (privateInfo->archive_streamer_buf->len != 0)
{
XLogRecPtr recPtr;
@@ -280,11 +296,19 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
}
#endif
+ /*
+ * Initialized to the beginning of the current segment being
+ * streamed through the buffer.
+ */
+ XLogSegNoOffsetToRecPtr(segNo, 0, WalSegSz,
+ privateInfo->archive_streamer_read_ptr);
+
/* Save the timeline */
privateInfo->timeline = timeline;
/* Update the next expected segment number */
- mystreamer->nextSegNo += 1;
+ mystreamer->nextSegNo =
+ member_next_segno(segNo, timeline, privateInfo);
}
break;
@@ -293,12 +317,44 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
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/%s\": %m",
+ privateInfo->tmpdir, 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;
+ }
break;
case ASTREAMER_ARCHIVE_TRAILER:
@@ -325,8 +381,14 @@ astreamer_waldump_finalize(astreamer *streamer)
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);
}
@@ -339,8 +401,8 @@ astreamer_waldump_free(astreamer *streamer)
static bool
member_is_relevant_wal(astreamer_member *member, TimeLineID startTimeLineID,
XLogSegNo startSegNo, XLogSegNo endSegNo,
- XLogSegNo nextSegNo, XLogSegNo *curSegNo,
- TimeLineID *curSegTimeline)
+ XLogSegNo nextSegNo, char **curFname,
+ XLogSegNo *curSegNo, TimeLineID *curSegTimeline)
{
int pathlen;
XLogSegNo segNo;
@@ -371,8 +433,85 @@ member_is_relevant_wal(astreamer_member *member, TimeLineID startTimeLineID,
if (startSegNo > segNo || endSegNo < segNo)
return false;
+ *curFname = fname;
*curSegNo = segNo;
*curSegTimeline = timeline;
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,
+ XLogDumpPrivate *privateInfo)
+{
+ FILE *file;
+ char *fpath = get_tmp_wal_file_path(privateInfo, 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
+
+ /* Record this segment's export */
+ simple_string_list_append(&privateInfo->exportedSegList, 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,
+ XLogDumpPrivate *privateInfo)
+{
+ 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(&privateInfo->exportedSegList, fname))
+ {
+ nextSegNo += 1;
+ exists = true;
+ }
+ } while (exists);
+
+ return nextSegNo;
+}
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index d136f8f038e..d57458d3148 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -394,13 +394,14 @@ setup_astreamer(XLogDumpPrivate *private, pg_compress_algorithm compression,
}
/*
- * Initializes the archive reader for a tar file.
+ * Initializes the tar archive reader and a temporary directory for WAL files.
*/
static void
init_tar_archive_reader(XLogDumpPrivate *private, char *waldir,
pg_compress_algorithm compression)
{
int fd;
+ char *tmpdir;
/* Now, the tar archive and store its file descriptor */
fd = open_file_in_directory(waldir, private->archive_name);
@@ -412,6 +413,15 @@ init_tar_archive_reader(XLogDumpPrivate *private, char *waldir,
/* Setup tar archive reading facility */
setup_astreamer(private, compression, private->startptr, private->endptr);
+
+ /* Temporary space for writing WAL segments */
+ if (getenv("TMPDIR"))
+ tmpdir = pg_strdup(getenv("TMPDIR"));
+ else
+ tmpdir = waldir != NULL ? pg_strdup(waldir) : pg_strdup(".");
+ canonicalize_path(tmpdir);
+
+ private->tmpdir = tmpdir;
}
/*
@@ -420,6 +430,8 @@ init_tar_archive_reader(XLogDumpPrivate *private, char *waldir,
static void
free_tar_archive_reader(XLogDumpPrivate *private)
{
+ SimpleStringListCell *cell;
+
/*
* NB: Normally, astreamer_finalize() is called before astreamer_free() to
* flush any remaining buffered data or to ensure the end of the tar
@@ -433,6 +445,15 @@ free_tar_archive_reader(XLogDumpPrivate *private)
if (close(private->archive_fd) != 0)
pg_log_error("could not close file \"%s\": %m",
private->archive_name);
+
+ /* Clear out any existing temporary files */
+ for (cell = private->exportedSegList.head; cell; cell = cell->next)
+ {
+ char *fpath = get_tmp_wal_file_path(private, cell->val);
+
+ unlink(fpath);
+ pfree(fpath);
+ }
}
/*
@@ -560,7 +581,7 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPtr, reqLen);
+ int count = required_read_len(private, targetPagePtr, reqLen);
WALReadError errinfo;
if (private->endptr_reached)
@@ -619,12 +640,58 @@ TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPtr, reqLen);
+ int count = required_read_len(private, targetPagePtr, reqLen);
+ XLogSegNo nextSegNo;
if (private->endptr_reached)
return -1;
- /* Read the WAL page from the archive streamer */
+ /*
+ * 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[MAXPGPATH];
+ 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(private, fname);
+ 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(&private->exportedSegList, fname))
+ {
+ fpath = get_tmp_wal_file_path(private, 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);
+ }
+ }
+
+ /* Continue reading from the open WAL segment, if any */
+ if (state->seg.ws_file >= 0)
+ return WALDumpReadPage(state, targetPagePtr, reqLen, targetPtr,
+ readBuff);
+
+ /* Otherwise, read the WAL page from the archive streamer */
return astreamer_wal_read(readBuff, targetPagePtr, count, private);
}
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
index b5d440500de..614e679cb96 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -13,8 +13,11 @@
#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;
/* Contains the necessary information to drive WAL decoding */
@@ -31,11 +34,30 @@ typedef struct XLogDumpPrivate
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 */
+ XLogRecPtr archive_streamer_read_ptr; /* Populate the buffer with
+ * records until this record
+ * pointer */
+ char *tmpdir; /* Temporary direcotry to export file */
+ SimpleStringList exportedSegList; /* Temporary exported WAL file list */
} XLogDumpPrivate;
+/*
+ * Generate the temporary WAL file path.
+ *
+ * Note that the caller is responsible to pfree it.
+ */
+static inline char *
+get_tmp_wal_file_path(XLogDumpPrivate *privateInfo, const char *fname)
+{
+ char *fpath = (char *) palloc(MAXPGPATH);
+
+ snprintf(fpath, MAXPGPATH, "%s/%s.%s", privateInfo->tmpdir, fname,
+ TEMP_FILE_EXT);
+
+ return fpath;
+}
+
extern astreamer *astreamer_waldump_content_new(astreamer *next,
XLogRecPtr startptr,
XLogRecPtr endptr,
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 443126a9ce6..d5fa1f6d28d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -7,6 +7,7 @@ use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+use List::Util qw(shuffle);
my $tar = $ENV{TAR};
@@ -272,7 +273,7 @@ sub generate_archive
}
closedir $dh;
- @files = sort @files;
+ @files = shuffle @files;
# move into the WAL directory before archiving files
my $cwd = getcwd;
--
2.47.1
v3-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchapplication/x-patch; name=v3-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchDownload
From 60c2ecfbe80203c73fb35d763eb63a9fad7fff45 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 14:47:43 +0530
Subject: [PATCH v3 6/8] pg_verifybackup: Delay default WAL directory
preparation.
We are not sure whether to parse WAL from a directory or an archive
until the backup format is known. Therefore, we delay preparing the
default WAL directory until the point of parsing. This delay is
harmless, as the WAL directory is not used elsewhere.
---
src/bin/pg_verifybackup/pg_verifybackup.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 5e6c13bb921..31ebc1581fb 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -285,10 +285,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
@@ -368,6 +364,10 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
+ /* 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 parse the required ranges of WAL records, unless we were told
* not to do so.
--
2.47.1
v3-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchapplication/x-patch; name=v3-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchDownload
From 18ce61a331aa5800cd3e42b13faaa3e9b39fdc7e Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 24 Jul 2025 16:37:43 +0530
Subject: [PATCH v3 7/8] pg_verifybackup: Rename the wal-directory switch to
wal-path
Future patches to pg_waldump will enable it to decode WAL directly
from tar files. This means you'll be able to specify a tar archive
path instead of a traditional WAL directory.
To keep things consistent and more versatile, we should also
generalize the input switch for pg_verifybackup. It should accept
either a directory or a tar file path that contains WALs. This change
will also aligning it with the existing manifest-path switch naming.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 2 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 22 +++++++++++-----------
src/bin/pg_verifybackup/po/de.po | 4 ++--
src/bin/pg_verifybackup/po/el.po | 4 ++--
src/bin/pg_verifybackup/po/es.po | 4 ++--
src/bin/pg_verifybackup/po/fr.po | 4 ++--
src/bin/pg_verifybackup/po/it.po | 4 ++--
src/bin/pg_verifybackup/po/ja.po | 4 ++--
src/bin/pg_verifybackup/po/ka.po | 4 ++--
src/bin/pg_verifybackup/po/ko.po | 4 ++--
src/bin/pg_verifybackup/po/ru.po | 4 ++--
src/bin/pg_verifybackup/po/sv.po | 4 ++--
src/bin/pg_verifybackup/po/uk.po | 4 ++--
src/bin/pg_verifybackup/po/zh_CN.po | 4 ++--
src/bin/pg_verifybackup/po/zh_TW.po | 4 ++--
src/bin/pg_verifybackup/t/007_wal.pl | 4 ++--
16 files changed, 40 insertions(+), 40 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index 61c12975e4a..e9b8bfd51b1 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -261,7 +261,7 @@ PostgreSQL documentation
<varlistentry>
<term><option>-w <replaceable class="parameter">path</replaceable></option></term>
- <term><option>--wal-directory=<replaceable class="parameter">path</replaceable></option></term>
+ <term><option>--wal-path=<replaceable class="parameter">path</replaceable></option></term>
<listitem>
<para>
Try to parse WAL files stored in the specified directory, rather than
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 31ebc1581fb..1ee400199da 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -93,7 +93,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 +126,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 +135,7 @@ 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 *pg_waldump_path = NULL;
DIR *dir;
@@ -221,8 +221,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 */
@@ -365,15 +365,15 @@ main(int argc, char **argv)
verify_backup_checksums(&context);
/* By default, look for the WAL in the backup directory, too. */
- if (wal_directory == NULL)
- wal_directory = psprintf("%s/pg_wal", context.backup_directory);
+ if (wal_path == NULL)
+ wal_path = psprintf("%s/pg_wal", context.backup_directory);
/*
* 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
@@ -1198,7 +1198,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 +1208,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 +1376,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 a9e24931100..9b5cd5898cf 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 3e3f20c67c5..81442f51c17 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 0cb958f3448..7f729fa35ba 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 da8c72f6427..09937966fa7 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 317b0b71e7f..4da68d0074e 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 c910fb236cc..a948959b54f 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 982751984c7..ef2799316a8 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 acdc3da5e02..eaf91ef1e98 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 64005feedfd..7fb0e5ab1f6 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 17240feeb5c..97125838e8c 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 034b9764232..63f8041ab38 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 b7d97c8976d..fb6fcae8b82 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 c1b710b0a36..568f972b0bb 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/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl
index babc4f0a86b..b07f80719b0 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', '--no-parse-wal', $backup_path ],
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: $!";
--
2.47.1
v3-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchapplication/x-patch; name=v3-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchDownload
From b3bd75e44fa50c89b52f650d4978fcb69d768652 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 17 Jul 2025 16:39:36 +0530
Subject: [PATCH v3 8/8] pg_verifybackup: enabled WAL parsing for tar-format
backup
Now that pg_waldump supports decoding from tar archives, we should
leverage this functionality to remove the previous restriction on WAL
parsing for tar-backed formats.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 5 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 66 +++++++++++++------
src/bin/pg_verifybackup/t/002_algorithm.pl | 4 --
src/bin/pg_verifybackup/t/003_corruption.pl | 4 +-
src/bin/pg_verifybackup/t/008_untar.pl | 3 +-
src/bin/pg_verifybackup/t/010_client_untar.pl | 3 +-
6 files changed, 50 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index e9b8bfd51b1..16b50b5a4df 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -36,10 +36,7 @@ PostgreSQL documentation
<literal>backup_manifest</literal> 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 <application>pg_basebackup</application>. However, at present,
- <literal>WAL</literal> verification is supported only for plain-format
- backups. Therefore, if the backup is stored in tar-format, the
- <literal>-n, --no-parse-wal</literal> option should be used.
+ supported by <application>pg_basebackup</application>.
</para>
<para>
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 1ee400199da..4bfe6fdff16 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);
@@ -136,6 +140,8 @@ main(int argc, char **argv)
bool no_parse_wal = false;
bool quiet = false;
char *wal_path = NULL;
+ char *base_archive_path = NULL;
+ char *wal_archive_path = NULL;
char *pg_waldump_path = NULL;
DIR *dir;
@@ -327,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'.
@@ -346,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.
@@ -364,9 +359,28 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
- /* By default, look for the WAL in the backup directory, too. */
+ /*
+ * 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)
- wal_path = psprintf("%s/pg_wal", context.backup_directory);
+ {
+ 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
@@ -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 <tablespaceoid>.tar file here. */
diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl
index ae16c11bc4d..4f284a9e828 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 1dd60f709cf..f1ebdbb46b4 100644
--- a/src/bin/pg_verifybackup/t/003_corruption.pl
+++ b/src/bin/pg_verifybackup/t/003_corruption.pl
@@ -193,10 +193,8 @@ for my $scenario (@scenario)
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/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl
index bc3d6b352ad..0cfe1f9532c 100644
--- a/src/bin/pg_verifybackup/t/008_untar.pl
+++ b/src/bin/pg_verifybackup/t/008_untar.pl
@@ -123,8 +123,7 @@ for my $tc (@test_configuration)
# 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 b62faeb5acf..76269a73673 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 @@ for my $tc (@test_configuration)
# 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");
--
2.47.1
On Tue, Aug 26, 2025 at 1:53 PM Amul Sul <sulamul@gmail.com> wrote:
[..patch]
Hi Amul!
0001: LGTM, maybe I would just slightly enhance the commit message
("This is in preparation for adding a second source file to this
directory.") -- maye bit a bit more verbose or use a message from
0002?
0002: LGTM
0003: LGTM
Tested here (after partial patch apply, and test suite did work fine).
0004:
a. Why should it be necessary to provide startLSN (-s) ? Couldn't
it autodetect the first WAL (tar file) inside and just use that with
some info message?
$ /usr/pgsql19/bin/pg_waldump --path=/tmp/base/pg_wal.tar
pg_waldump: error: no start WAL location given
b. Why would it like to open "blah" dir if I wanted that "blah"
segment from the archive? Shouldn't it tell that it was looking in the
archive and couldn find it inside?
$ /usr/pgsql19/bin/pg_waldump --path=/tmp/base/pg_wal.tar blah
pg_waldump: error: could not open file "blah": Not a directory
c. It doesnt work when using SEGSTART, but it's there:
$ /usr/pgsql19/bin/pg_waldump --path=/tmp/base/pg_wal.tar
000000010000000000000059
pg_waldump: error: could not open file "000000010000000000000059":
Not a directory
$ tar tf /tmp/base/pg_wal.tar | head -1
000000010000000000000059
d. I've later noticed that follow-up patches seem to use the
-s switch and there it seems to work OK. The above SEGSTART issue was
not detected, probably because tests need to be extended cover of
segment name rather than just --start LSN (see test_pg_waldump):
$ /usr/pgsql19/bin/pg_waldump --path=/tmp/base/pg_wal.tar --stats
-s 0/59000358
pg_waldump: first record is after 0/59000358, at 0/590003E8,
skipping over 144 bytes
WAL statistics between 0/590003E8 and 0/61000000:
[..]
e. Code around`if (walpath == NULL && directory != NULL)` needs
some comments.
f. Code around `if (fname != NULL && is_tar_file(fname,
&compression))` , so if fname is WAL segment here
(00000001000000000000005A) and we do check again if that has been
tar-ed (is_tar_file())? Why?
g. Just a question: the commit message says `Note that this patch
requires that the WAL files within the archive be in sequential order;
an error will be reported otherwise`. I'm wondering if such
occurrences are known to be happening in the wild? Or is it just an
assumption that if someone would modify the tar somehow? (either way
we could just add a reason why we need to handle such a case if we
know -- is manual alternation the only source of such state?). For the
record, I've tested crafting custom archives with out of sequence WAL
archives and the code seems to work (it was done using: tar --append
-f pg_wal.tar --format=ustar ..)
h. Anyway, in case of typo/wrong LSN, 0004 emits wrong error
message I think:
$ /usr/pgsql19/bin/pg_waldump --path=/tmp/base/pg_wal.tar --stats
-s 0/50000358
pg_waldump: error: WAL files are not archived in sequential order
pg_waldump: detail: Expecting segment number 80 but found 89.
it's just that the 50000358 LSN above is below the minimal LSN
present in the WAL segments (first segment is 000000010000000000000059
there, i've just intentionally provided a bad value 50.. as a typo and
it causes the wrong message). Now it might not be an issue as with
0005 patch the same test behaves OK (`pg_waldump: error: could not
find a valid record after 0/50000358`). It is just relevant if this
would be committed not all at once.
i. If I give wrong --timeline=999 to pg_waldump it fails with
misleading error message: could not read WAL data from "pg_wal.tar"
archive: read -1 of 8192
0005:
a. I'm wondering if we shouldn't log (to stderr?) some kind of
notification message (just once) that non-sequential WAL files were
discovered and that pg_waldump is starting to write to $somewhere as
it may be causing bigger I/O than anticipated when running the
command. This can easily help when troubleshooting why it is not fast,
and also having set TMPDIR to usually /tmp can be slow or too small.
b. IMHO member_prepare_tmp_write() / get_tmp_wal_file_path() with
TMPDIR can be prone to symlink attack. Consider setting TMPDIR=/tmp .
We are writing to e.g. /tmp/<WALsegment>.waldump.tmp in 0004 , but
that path is completely guessable. If an attacker prepares some
symlinks and links those to some other places, I think the code will
happily open and overwrite the contents of the rogue symlink. I think
using mkstemp(3)/tmpfile(3) would be a safer choice if TMPDIR needs to
be in play. Consider that pg_waldump can be run as root (there's no
mechanism preventing it from being used that way).
c. IMHO that unlink() might be not guaranteed to always remove
files, as in case of any trouble and exit() , those files might be
left over. I think we need some atexit() handlers. This can be
triggered with combo of options of nonsequential files in tar + wrong
LSN given:
$ tar tf pg_wal.tar
00000001000000000000005A
00000001000000000000005B
00000001000000000000005C
[..]
000000010000000000000060
000000010000000000000059 <-- out of order, appended last
$ ls -lh 0*
ls: cannot access '0*': No such file or directory
$ /usr/pgsql19/bin/pg_waldump --path=/tmp/ble/pg_wal.tar --stats
-s 0/10000358 #wrong LSN
pg_waldump: error: could not find a valid record after 0/10000358
$ ls -lh 0*
-rw------- 1 postgres postgres 16M Sep 8 14:44
000000010000000000000059.waldump.tmp
-rw------- 1 postgres postgres 16M Sep 8 14:44
00000001000000000000005A.waldump.tmp
[..]
0006: LGTM
0007:
a. Commit message says `Future patches to pg_waldump will enable
it to decode WAL directly` , but those pg_waldump are earlier patches,
right?
b. pg_verifybackup should print some info with --progress that it
is spawning pg_waldump (pg_verifybackup --progress mode does not
display anything related to verifing WALs, but it could)
c. I'm wondering, but pg_waldump seems to be not complaining if
--end=LSN is made into such a future that it doesn't exist. E.g. If
the latest WAL segment is 60 (with end LSN 0/60A77A59), but I run
pg_waldump `--end=0/7000000` , it will return code 0 and nothing on
stderr. So how sure are we that the necessary WAL segments (as per
backup_manifest) are actually inside the tar? It's supposed to be
verified, but it isn't for this use case? Same happens if craft
special tar and remove just one WAL segment from pg_wal.tar (simulate
missing WAL segment), but ask the pg_verifybackup/pg_waldump to verify
it to exact last LSN sequence, e.g.:
$ /usr/pgsql19/bin/pg_waldump --quiet
--path=/tmp/missing/pg_wal.tar --timeline=1 --start=0/59000028
--end=0/60A77A58 && echo OK # but it is not OK
OK
$ /usr/pgsql19/bin/pg_waldump --stats
--path=/tmp/missing/pg_wal.tar --timeline=1 --start=0/59000028
--end=0/60A77A58
WAL statistics between 0/59000028 and 0/5CFFFFD0: # <-- 0/5C LSN
maximum detected
[..]
Notice it has read till 0/5C (but I've asked till 0/60), because
I've removed 0D:
$ tar tf /tmp/missing/pg_wal.tar| grep ^0
000000010000000000000059
00000001000000000000005A
00000001000000000000005B
00000001000000000000005C
00000001000000000000005E <-- missing 5D
Yet it reported no errors.
0008:
LGTM
Another open question I have is this: shouldn't backup_manifest come
with CRC checksum for the archived WALs? Or does that guarantee that
backup_manifest WAL-Ranges are present in pg_wal.tar is good enough
because individual WAL files are CRC-protected itself?
-J.
On Mon, Sep 8, 2025 at 7:07 PM Jakub Wartak
<jakub.wartak@enterprisedb.com> wrote:
On Tue, Aug 26, 2025 at 1:53 PM Amul Sul <sulamul@gmail.com> wrote:
[..patch]
Hi Amul!
Thanks for your review. I'm replying to a few of your comments now,
but for the rest, I need to think about them. I'm kind of in agreement
with some of them for the fix, but I won't be able to spend time on
that next week due to official travel. I'll try to get back as soon as
possible after that.
a. Why should it be necessary to provide startLSN (-s) ? Couldn't
it autodetect the first WAL (tar file) inside and just use that with
some info message?
$ /usr/pgsql19/bin/pg_waldump --path=/tmp/base/pg_wal.tar
pg_waldump: error: no start WAL location given
There are two reasons. First, existing pg_waldump
--path=some_directory would result in the same error. Second, it would
force us to re-read the archive twice just to locate the first WAL
segment, which is inefficient.
c. It doesnt work when using SEGSTART, but it's there:
$ /usr/pgsql19/bin/pg_waldump --path=/tmp/base/pg_wal.tar
000000010000000000000059
pg_waldump: error: could not open file "000000010000000000000059":
Not a directory
$ tar tf /tmp/base/pg_wal.tar | head -1
000000010000000000000059
I don't believe this is the correct use case. The WAL files are inside
a tar archive, and the requirement is to use a starting LSN and a
timeline (if not the default).
d. I've later noticed that follow-up patches seem to use the
-s switch and there it seems to work OK. The above SEGSTART issue was
not detected, probably because tests need to be extended cover of
segment name rather than just --start LSN (see test_pg_waldump):
$ /usr/pgsql19/bin/pg_waldump --path=/tmp/base/pg_wal.tar --stats
-s 0/59000358
pg_waldump: first record is after 0/59000358, at 0/590003E8,
skipping over 144 bytes
WAL statistics between 0/590003E8 and 0/61000000:
[..]
Hope previous reasoning makes sense to you.
e. Code around`if (walpath == NULL && directory != NULL)` needs
some comments.
I think this is an existing one.
f. Code around `if (fname != NULL && is_tar_file(fname,
&compression))` , so if fname is WAL segment here
(00000001000000000000005A) and we do check again if that has been
tar-ed (is_tar_file())? Why?
Again, how?
g. Just a question: the commit message says `Note that this patch
requires that the WAL files within the archive be in sequential order;
an error will be reported otherwise`. I'm wondering if such
occurrences are known to be happening in the wild? Or is it just an
assumption that if someone would modify the tar somehow? (either way
we could just add a reason why we need to handle such a case if we
know -- is manual alternation the only source of such state?). For the
record, I've tested crafting custom archives with out of sequence WAL
archives and the code seems to work (it was done using: tar --append
-f pg_wal.tar --format=ustar ..)
This is an almost nonexistent occurrence. While pg_basebackup archives
WAL files in sequential order, we don't have an explicit code to
enforce that order within it. Furthermore, since we can't control how
external tools might handle the files, this extra precaution is
necessary.
Another open question I have is this: shouldn't backup_manifest come
with CRC checksum for the archived WALs? Or does that guarantee that
backup_manifest WAL-Ranges are present in pg_wal.tar is good enough
because individual WAL files are CRC-protected itself?
I don't know, I have to check pg_verifybackup.
Regards,
Amul
Here are some review comments on v3-0004:
In general, I think this looks pretty nice, but I think it needs more
cleanup and polishing.
There doesn't seem to be any reason for
astreamer_waldump_content_new() to take an astreamer *next argument.
If you look at astreamer.h, you'll see that some astreamer_BLAH_new()
functions take such an argument, and others don't. The ones that do
forward their input to another astreamer; the ones that don't, like
astreamer_plain_writer_new(), send it somewhere else. AFAICT, this
astreamer is never going to send its output to another astreamer, so
there's no reason for this argument.
I'm also a little confused by the choice of the name
astreamer_waldump_content_new(). I would have thought this would be
something like astreamer_waldump_new() or astreamer_xlogreader_new().
The word "content" doesn't seem to me to be adding much here, and it
invites confusion with the "content" callback.
I think you can merge setup_astreamer() into
init_tar_archive_reader(). The only other caller is
verify_tar_archive(), but that does exactly the same additional steps
as init_tar_archive_reader(), as far as I can see.
The return statement for astreamer_wal_read is really odd:
+ return (count - nbytes) ? (count - nbytes) : -1;
Since 0 is false in C, this is equivalent to: count != nbytes ? count
- nbytes : -1, but it's a strange way to write it. What makes it even
stranger is that it seems as though the intention here is to count the
number of bytes read, but you do that by taking the number of bytes
requested (count) and subtracting the number of bytes we didn't manage
to read (nbytes); and then you just up and return -1 instead of 0
whenever the answer would have been zero. This is all lacking in
comments and seems a bit more confusing than it needs to be. So my
suggestions are:
1. Consider redefining nbytes to be the number of bytes that you have
read instead of the number of bytes you haven't read. So the loop in
this function would be while (nbytes < count) instead of while (nbytes
0).
2. If you need to map 0 to -1, consider having the caller do this
instead of putting that inside this function.
3. Add a comment saying what the return value is supposed to be".
If you do both 1 and 2, then the return statement can just say "return
nbytes;" and the comment can say "Returns the number of bytes
successfully read."
I would suggest changing the name of the variable from "readBuff" to
"readBuf". There are no existing uses of readBuff in the code base.
I think this comment also needs improvement:
+ /*
+ * Ignore existing data if the required target page
has not yet been
+ * read.
+ */
+ if (recptr >= endPtr)
+ {
+ len = 0;
+
+ /* Reset the buffer */
+ resetStringInfo(astreamer_buf);
+ }
This comment is problematic for a few reasons. First, we're not
ignoring the existing data: we're throwing it out. Second, the comment
doesn't say why we're doing what we're doing, only that we're doing
it. Here's my guess at the actual explanation -- please correct me if
I'm wrong: "pg_waldump never reads 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." By the way, if this explanation is
correct, it might be nice to add an assertion someplace that verifies
it, like asserting that we're always reading from an LSN greater than
or equal to (or exactly equal to?) the LSN immediately following the
last data we read.
In general, I wonder whether there's a way to make the separation of
concerns between astreamer_wal_read() and TarWALDumpReadPage()
cleaner. Right now, the latter is basically a stub, but I'm not sure
that is the best thing here. I already mentioned one example of how to
do this: make the responsibility for 0 => -1 translation the job of
TarWALDumpReadPage() rather than astreamer_wal_read(). But I think
there might be a little more we can do. In particular, I wonder
whether we could say that astreamer_wal_read() is only responsible for
filling the buffer, and the caller, TarWALDumpReadPage() in this case,
needs to empty it. That seems like it might produce a cleaner
separation of duties.
Another thing that isn't so nice right now is that
verify_tar_archive() has to open and close the archive only for
init_tar_archive_reader() to be called to reopen it again just moments
later. It would be nicer to open the file just once and then keep it
open. Here again, I wonder if the separation of duties could be a bit
cleaner.
Is there a real need to pass XLogDumpPrivate to astreamer_wal_read or
astreamer_archive_read? The only things that they need are archive_fd,
archive_name, archive_streamer, archive_streamer_buf, and
archive_streamer_read_ptr. In other words, they really don't care
about any of the *existing* things that are in XLogDumpPrivate. This
makes me wonder whether we should actually try to make this new
astreamer completely independent of xlogreader. In other words,
instead of calling it astreamer_waldump() or astreamer_xlogreader() as
I proposed above, maybe it could be a completely generic astreamer,
say astreamer_stringinfo_new(StringInfo *buf) that just appends to the
buffer. That would require also moving the stuff out of
astreamer_wal_read() that knows about XLogRecPtr, but why does that
function need to know about XLogRecPtr? Couldn't the caller figure out
that part and just tell this function how many bytes are needed?
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Sep 12, 2025 at 2:28 PM Robert Haas <robertmhaas@gmail.com> wrote:
Is there a real need to pass XLogDumpPrivate to astreamer_wal_read or
astreamer_archive_read? The only things that they need are archive_fd,
archive_name, archive_streamer, archive_streamer_buf, and
archive_streamer_read_ptr. In other words, they really don't care
about any of the *existing* things that are in XLogDumpPrivate. This
makes me wonder whether we should actually try to make this new
astreamer completely independent of xlogreader. In other words,
instead of calling it astreamer_waldump() or astreamer_xlogreader() as
I proposed above, maybe it could be a completely generic astreamer,
say astreamer_stringinfo_new(StringInfo *buf) that just appends to the
buffer. That would require also moving the stuff out of
astreamer_wal_read() that knows about XLogRecPtr, but why does that
function need to know about XLogRecPtr? Couldn't the caller figure out
that part and just tell this function how many bytes are needed?
Hmm, on further thought, I think this was a silly idea. Part of the
intended function of this astreamer is to make sure we're only reading
WAL files from the archive, and eventually reordering them if
required, so obviously something completely generic isn't going to
work. Maybe there's a way to make this look a little cleaner and
tidier but this isn't it...
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Sep 12, 2025 at 4:25 PM Amul Sul <sulamul@gmail.com> wrote:
On Mon, Sep 8, 2025 at 7:07 PM Jakub Wartak
<jakub.wartak@enterprisedb.com> wrote:On Tue, Aug 26, 2025 at 1:53 PM Amul Sul <sulamul@gmail.com> wrote:
[..patch]
Hi Amul!
Thanks for your review. I'm replying to a few of your comments now,
but for the rest, I need to think about them. I'm kind of in agreement
with some of them for the fix, but I won't be able to spend time on
that next week due to official travel. I'll try to get back as soon as
possible after that.
Reverting on rest of review comments:
0001: LGTM, maybe I would just slightly enhance the commit message
("This is in preparation for adding a second source file to this
directory.") -- maye bit a bit more verbose or use a message from
0002?
Done.
b. Why would it like to open "blah" dir if I wanted that "blah"
segment from the archive? Shouldn't it tell that it was looking in the
archive and couldn find it inside?
$ /usr/pgsql19/bin/pg_waldump --path=/tmp/base/pg_wal.tar blah
pg_waldump: error: could not open file "blah": Not a directory
Now, an error will be thrown if any additional command-line
arguments are provided when an archive is specified, similar to how
existing extra arguments are handled.
i. If I give wrong --timeline=999 to pg_waldump it fails with
misleading error message: could not read WAL data from "pg_wal.tar"
archive: read -1 of 8192
Now., added a much better error message for that case.
a. I'm wondering if we shouldn't log (to stderr?) some kind of
notification message (just once) that non-sequential WAL files were
discovered and that pg_waldump is starting to write to $somewhere as
it may be causing bigger I/O than anticipated when running the
command. This can easily help when troubleshooting why it is not fast,
and also having set TMPDIR to usually /tmp can be slow or too small.
Now, emitting info messages, but I'm not sure whether we should have
info or debug.
b. IMHO member_prepare_tmp_write() / get_tmp_wal_file_path() with
TMPDIR can be prone to symlink attack. Consider setting TMPDIR=/tmp .
We are writing to e.g. /tmp/<WALsegment>.waldump.tmp in 0004 , but
that path is completely guessable. If an attacker prepares some
symlinks and links those to some other places, I think the code will
happily open and overwrite the contents of the rogue symlink. I think
using mkstemp(3)/tmpfile(3) would be a safer choice if TMPDIR needs to
be in play. Consider that pg_waldump can be run as root (there's no
mechanism preventing it from being used that way).
I am not sure what the worst-case scenario would be or what a good
alternative is.
c. IMHO that unlink() might be not guaranteed to always remove
files, as in case of any trouble and exit() , those files might be
left over. I think we need some atexit() handlers. This can be
triggered with combo of options of nonsequential files in tar + wrong
LSN given:
Done.
0007:
a. Commit message says `Future patches to pg_waldump will enable
it to decode WAL directly` , but those pg_waldump are earlier patches,
right?
Right, fixed.
b. pg_verifybackup should print some info with --progress that it
is spawning pg_waldump (pg_verifybackup --progress mode does not
display anything related to verifing WALs, but it could)
If we decide to do that, it could be a separate project, IMHO.
c. I'm wondering, but pg_waldump seems to be not complaining if
--end=LSN is made into such a future that it doesn't exist.
The behavior will be kept as if a directory was provided with a start
and end LSN.
Thanks again for the review. I'll post the new patches in my next reply.
Regards,
Amul
On Fri, Sep 12, 2025 at 11:58 PM Robert Haas <robertmhaas@gmail.com> wrote:
Here are some review comments on v3-0004:
Thanks for the review. My replies are below.
There doesn't seem to be any reason for
astreamer_waldump_content_new() to take an astreamer *next argument.
If you look at astreamer.h, you'll see that some astreamer_BLAH_new()
functions take such an argument, and others don't. The ones that do
forward their input to another astreamer; the ones that don't, like
astreamer_plain_writer_new(), send it somewhere else. AFAICT, this
astreamer is never going to send its output to another astreamer, so
there's no reason for this argument.
Done.
I'm also a little confused by the choice of the name
astreamer_waldump_content_new(). I would have thought this would be
something like astreamer_waldump_new() or astreamer_xlogreader_new().
The word "content" doesn't seem to me to be adding much here, and it
invites confusion with the "content" callback.
Done -- renamed to astreamer_waldump_new().
I think you can merge setup_astreamer() into
init_tar_archive_reader(). The only other caller is
verify_tar_archive(), but that does exactly the same additional steps
as init_tar_archive_reader(), as far as I can see.
Done.
The return statement for astreamer_wal_read is really odd:
+ return (count - nbytes) ? (count - nbytes) : -1;
Agreed, that's a bit odd. This seems to be leftover code from the experimental
patch. The astreamer_wal_read() function should behave like WALRead():
it should either successfully read all the requested bytes or throw an
error. Corrected in the attached version.
I would suggest changing the name of the variable from "readBuff" to
"readBuf". There are no existing uses of readBuff in the code base.
The existing WALDumpReadPage() function has a "readBuff" argument, and
I've used it that way for consistency.
I think this comment also needs improvement:
+ /* + * Ignore existing data if the required target page has not yet been + * read. + */ + if (recptr >= endPtr) + { + len = 0; + + /* Reset the buffer */ + resetStringInfo(astreamer_buf); + }This comment is problematic for a few reasons. First, we're not
ignoring the existing data: we're throwing it out. Second, the comment
doesn't say why we're doing what we're doing, only that we're doing
it. Here's my guess at the actual explanation -- please correct me if
I'm wrong: "pg_waldump never reads 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." By the way, if this explanation is
correct, it might be nice to add an assertion someplace that verifies
it, like asserting that we're always reading from an LSN greater than
or equal to (or exactly equal to?) the LSN immediately following the
last data we read.
Updated the comment. The similar assertion exists right before
copying to the readBuff.
Another thing that isn't so nice right now is that
verify_tar_archive() has to open and close the archive only for
init_tar_archive_reader() to be called to reopen it again just moments
later. It would be nicer to open the file just once and then keep it
open. Here again, I wonder if the separation of duties could be a bit
cleaner.
Prefer to keep those separate, assuming that reopening the file won't
cause any significant harm. Let me know if you think otherwise.
Attached the updated version, kindly have a look.
Regards,
Amul
Attachments:
v4-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchapplication/x-patch; name=v4-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchDownload
From 8eb84b553d856bbbffda254e419152c236346848 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 24 Jun 2025 11:33:20 +0530
Subject: [PATCH v4 1/8] Refactor: pg_waldump: Move some declarations to new
pg_waldump.h
This change prepares for a second source file in this directory to
support reading WAL from tar files. Common structures, declarations,
and functions are being exported through this include file so
they can be used in both files.
---
src/bin/pg_waldump/pg_waldump.c | 11 ++---------
src/bin/pg_waldump/pg_waldump.h | 27 +++++++++++++++++++++++++++
2 files changed, 29 insertions(+), 9 deletions(-)
create mode 100644 src/bin/pg_waldump/pg_waldump.h
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 13d3ec2f5be..a49b2fd96c7 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,19 +40,11 @@
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};
-typedef struct XLogDumpPrivate
-{
- TimeLineID timeline;
- XLogRecPtr startptr;
- XLogRecPtr endptr;
- bool endptr_reached;
-} XLogDumpPrivate;
-
typedef struct XLogDumpConfig
{
/* display options */
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
new file mode 100644
index 00000000000..9e62b64ead5
--- /dev/null
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -0,0 +1,27 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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"
+
+extern int WalSegSz;
+
+/* Contains the necessary information to drive WAL decoding */
+typedef struct XLogDumpPrivate
+{
+ TimeLineID timeline;
+ XLogRecPtr startptr;
+ XLogRecPtr endptr;
+ bool endptr_reached;
+} XLogDumpPrivate;
+
+#endif /* end of PG_WALDUMP_H */
--
2.47.1
v4-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchapplication/x-patch; name=v4-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchDownload
From 9f719d5744c293a91aca5a933b357296180281ff Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 26 Jun 2025 11:42:53 +0530
Subject: [PATCH v4 2/8] Refactor: pg_waldump: Separate logic used to calculate
the required read size.
This refactoring prepares the codebase for an upcoming patch that will
support reading WAL from tar files. The logic for calculating the
required read size has been updated to handle both normal WAL files
and WAL files located inside a tar archive.
---
src/bin/pg_waldump/pg_waldump.c | 39 ++++++++++++++++++++++-----------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index a49b2fd96c7..8d0cd9e7156 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -326,6 +326,29 @@ identify_target_directory(char *directory, char *fname)
return NULL; /* not reached */
}
+/* 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,
@@ -383,21 +406,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))
--
2.47.1
v4-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchapplication/x-patch; name=v4-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchDownload
From f51ddb6d02ef6e3383beebdd4486d10191499955 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 30 Jul 2025 12:43:30 +0530
Subject: [PATCH v4 3/8] Refactor: pg_waldump: Restructure TAP tests.
Restructured some tests to run inside a loop, facilitating their
re-execution for decoding WAL from tar archives.
---
src/bin/pg_waldump/t/001_basic.pl | 123 ++++++++++++++++--------------
1 file changed, 67 insertions(+), 56 deletions(-)
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index f26d75e01cf..1b712e8d74d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -198,28 +198,6 @@ command_like(
],
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 +205,6 @@ command_like(
],
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.
@@ -272,7 +241,6 @@ sub test_pg_waldump
my $result = IPC::Run::run [
'pg_waldump',
- '--path' => $node->data_dir,
'--start' => $start_lsn,
'--end' => $end_lsn,
@opts
@@ -288,38 +256,81 @@ sub test_pg_waldump
my @lines;
-@lines = test_pg_waldump;
-is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
+my @scenario = (
+ {
+ 'path' => $node->data_dir
+ });
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenario)
+{
+ my $path = $scenario->{'path'};
-@lines = test_pg_waldump('--fullpage');
-is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
+ SKIP:
+ {
+ 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');
-@lines = test_pg_waldump('--stats');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+ command_fails_like(
+ [
+ 'pg_waldump', '--quiet',
+ '--path' => $path,
+ '--start' => $start_lsn
+ ],
+ qr/error: error in WAL record at/,
+ 'errors are shown with --quiet');
-@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('--path' => $path);
+ is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
-@lines = test_pg_waldump('--rmgr' => 'Btree');
-is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+ @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ is(@lines, 6, 'limit option observed');
-@lines = test_pg_waldump('--fork' => 'init');
-is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+ @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
-@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('--path' => $path, '--stats');
+ like($lines[0], qr/WAL statistics/, "statistics on stdout");
+ is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
-@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');
+ @lines = test_pg_waldump('--path' => $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' => $path, '--rmgr' => 'Btree');
+ is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+
+ @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+
+ @lines = test_pg_waldump('--path' => $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' => $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');
+ }
+}
done_testing();
--
2.47.1
v4-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchapplication/x-patch; name=v4-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchDownload
From be0cf9b0d4ff99630298e499f93d09a17eeae141 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 18:37:59 +0530
Subject: [PATCH v4 4/8] pg_waldump: Add support for archived WAL decoding.
pg_waldump can now accept the path to a tar archive containing WAL
files and decode them. This feature was added primarily for
pg_verifybackup, which previously disabled WAL parsing for
tar-formatted backups.
Note that this patch requires that the WAL files within the archive be
in sequential order; an error will be reported otherwise. The next
patch is planned to remove this restriction.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/Makefile | 7 +-
src/bin/pg_waldump/astreamer_waldump.c | 388 +++++++++++++++++++++++++
src/bin/pg_waldump/meson.build | 4 +-
src/bin/pg_waldump/pg_waldump.c | 365 +++++++++++++++++++----
src/bin/pg_waldump/pg_waldump.h | 20 +-
src/bin/pg_waldump/t/001_basic.pl | 84 +++++-
src/tools/pgindent/typedefs.list | 1 +
8 files changed, 799 insertions(+), 78 deletions(-)
create mode 100644 src/bin/pg_waldump/astreamer_waldump.c
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index ce23add5577..d004bb0f67e 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -141,13 +141,17 @@ PostgreSQL documentation
<term><option>--path=<replaceable>path</replaceable></option></term>
<listitem>
<para>
- Specifies a directory to search for WAL segment files or a
- directory with a <literal>pg_wal</literal> subdirectory that
+ Specifies a tar archive or a directory to search for WAL segment files
+ or a directory with a <literal>pg_wal</literal> subdirectory that
contains such files. The default is to search in the current
directory, the <literal>pg_wal</literal> subdirectory of the
current directory, and the <literal>pg_wal</literal> subdirectory
of <envar>PGDATA</envar>.
</para>
+ <para>
+ If a tar archive is provided, its WAL segment files must be in
+ sequential order; otherwise, an error will be reported.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile
index 4c1ee649501..b234613eb50 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 00000000000..caf7da6ccb8
--- /dev/null
+++ b/src/bin/pg_waldump/astreamer_waldump.c
@@ -0,0 +1,388 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 <unistd.h>
+
+#include "access/xlog_internal.h"
+#include "access/xlogdefs.h"
+#include "common/logging.h"
+#include "fe_utils/simple_list.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;
+ XLogSegNo nextSegNo; /* Next expected segment to stream */
+} astreamer_waldump;
+
+static int astreamer_archive_read(XLogDumpPrivate *privateInfo);
+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,
+ XLogSegNo *curSegNo);
+
+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.
+ */
+static 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);
+
+ /*
+ * Initialize the record pointer to the beginning of the first
+ * segment; this pointer will track the WAL record reading status.
+ */
+ XLogSegNoOffsetToRecPtr(streamer->startSegNo, 0, WalSegSz,
+ privateInfo->archive_streamer_read_ptr);
+ }
+
+ 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:
+ {
+ XLogSegNo segNo;
+
+ pg_log_debug("pg_waldump: reading \"%s\"", member->pathname);
+
+ mystreamer->skipThisSeg = false;
+
+ if (!member_is_relevant_wal(mystreamer, member,
+ privateInfo->timeline, &segNo))
+ {
+ 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;
+
+ /* WAL segments must be archived in order */
+ if (mystreamer->nextSegNo != segNo)
+ {
+ pg_log_error("WAL files are not archived in sequential order");
+ pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
+ mystreamer->nextSegNo, segNo);
+ exit(1);
+ }
+
+ /*
+ * We track the reading of WAL segment records using a pointer
+ * that's continuously incremented by the length of the
+ * received data. This pointer is crucial for serving WAL page
+ * requests from the WAL decoding routine, so it must be
+ * accurate.
+ */
+#ifdef USE_ASSERT_CHECKING
+ if (mystreamer->nextSegNo != 0)
+ {
+ XLogRecPtr recPtr;
+
+ XLogSegNoOffsetToRecPtr(segNo, 0, WalSegSz, recPtr);
+ Assert(privateInfo->archive_streamer_read_ptr == recPtr);
+ }
+#endif
+ /* Update the next expected segment number */
+ mystreamer->nextSegNo += 1;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_CONTENTS:
+ /* Skip this segment */
+ if (mystreamer->skipThisSeg)
+ break;
+
+ /* Or, copy contents to buffer */
+ privateInfo->archive_streamer_read_ptr += len;
+ astreamer_buffer_bytes(streamer, &data, &len, len);
+ break;
+
+ case ASTREAMER_MEMBER_TRAILER:
+ 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)
+{
+ Assert(streamer->bbs_next == NULL);
+
+ 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, 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;
+ }
+
+ *curSegNo = segNo;
+
+ return true;
+}
diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build
index 937e0d68841..2a0300dc339 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 8d0cd9e7156..393d6bfa9ef 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -326,6 +326,148 @@ 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;
+}
+
+/*
+ * 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;
+}
+
+/*
+ * 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,
@@ -406,7 +548,7 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPagePtr, reqLen);
+ int count = required_read_len(private, targetPtr, reqLen);
WALReadError errinfo;
if (private->endptr_reached)
@@ -436,6 +578,44 @@ 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, targetPtr, reqLen);
+
+ if (private->endptr_reached)
+ return -1;
+
+ /* 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.
@@ -773,8 +953,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"
@@ -806,7 +986,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'},
@@ -938,7 +1121,7 @@ main(int argc, char **argv)
}
break;
case 'p':
- waldir = pg_strdup(optarg);
+ walpath = pg_strdup(optarg);
break;
case 'q':
config.quiet = true;
@@ -1102,10 +1285,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;
@@ -1123,46 +1316,36 @@ 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;
+ waldir = walpath ? pg_strdup(walpath) : pg_strdup(".");
+ 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)
@@ -1170,32 +1353,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);
+
+ 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);
- 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;
+ /* 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))
@@ -1207,12 +1428,31 @@ 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);
+
+ /* 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");
@@ -1321,6 +1561,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
index 9e62b64ead5..4205e0ef597 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -12,6 +12,8 @@
#define PG_WALDUMP_H
#include "access/xlogdefs.h"
+#include "fe_utils/astreamer.h"
+#include "lib/stringinfo.h"
extern int WalSegSz;
@@ -22,6 +24,22 @@ typedef struct XLogDumpPrivate
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 */
} XLogDumpPrivate;
-#endif /* end of PG_WALDUMP_H */
+
+extern astreamer *astreamer_waldump_new(XLogRecPtr startptr,
+ XLogRecPtr endptr,
+ XLogDumpPrivate *privateInfo);
+extern int astreamer_wal_read(char *readBuff, XLogRecPtr startptr, Size count,
+ 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 1b712e8d74d..443126a9ce6 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -3,10 +3,13 @@
use strict;
use warnings FATAL => 'all';
+use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $tar = $ENV{TAR};
+
program_help_ok('pg_waldump');
program_version_ok('pg_waldump');
program_options_handling_ok('pg_waldump');
@@ -235,7 +238,7 @@ command_like(
sub test_pg_waldump
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my @opts = @_;
+ my ($path, @opts) = @_;
my ($stdout, $stderr);
@@ -243,6 +246,7 @@ sub test_pg_waldump
'pg_waldump',
'--start' => $start_lsn,
'--end' => $end_lsn,
+ '--path' => $path,
@opts
],
'>' => \$stdout,
@@ -254,11 +258,50 @@ sub test_pg_waldump
return @lines;
}
-my @lines;
+# 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 = sort @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
+ '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)
@@ -267,6 +310,19 @@ for my $scenario (@scenario)
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/,
@@ -298,38 +354,42 @@ for my $scenario (@scenario)
qr/error: error in WAL record at/,
'errors are shown with --quiet');
- @lines = test_pg_waldump('--path' => $path);
+ my @lines;
+ @lines = test_pg_waldump($path);
is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
- @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ @lines = test_pg_waldump($path, '--limit' => 6);
is(@lines, 6, 'limit option observed');
- @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ @lines = test_pg_waldump($path, '--fullpage');
is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
- @lines = test_pg_waldump('--path' => $path, '--stats');
+ @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' => $path, '--stats=record');
+ @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' => $path, '--rmgr' => 'Btree');
+ @lines = test_pg_waldump($path, '--rmgr' => 'Btree');
is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
- @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ @lines = test_pg_waldump($path, '--fork' => 'init');
is(grep(!/fork init/, @lines), 0, 'only init fork lines');
- @lines = test_pg_waldump('--path' => $path,
+ @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' => $path,
+ @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'};
}
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a13e8162890..b406ca041ec 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3444,6 +3444,7 @@ astreamer_recovery_injector
astreamer_tar_archiver
astreamer_tar_parser
astreamer_verify
+astreamer_waldump
astreamer_zstd_frame
auth_password_hook_typ
autovac_table
--
2.47.1
v4-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchapplication/x-patch; name=v4-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchDownload
From d79a505af9532baae675557de0efb1bcc35d72f5 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Mon, 25 Aug 2025 17:26:29 +0530
Subject: [PATCH v4 5/8] pg_waldump: Remove the restriction on the order of
archived WAL files.
With previous patch, pg_waldump would stop decoding if WAL files were
not in the required sequence. With this patch, decoding will now
continue. Any WAL file that is out of order will be written to a
temporary location, from which it will be read later. Once a temporary
file has been read, it will be removed.
---
doc/src/sgml/ref/pg_waldump.sgml | 7 +-
src/bin/pg_waldump/astreamer_waldump.c | 214 +++++++++++++++++++++----
src/bin/pg_waldump/pg_waldump.c | 112 ++++++++++++-
src/bin/pg_waldump/pg_waldump.h | 30 +++-
src/bin/pg_waldump/t/001_basic.pl | 3 +-
5 files changed, 323 insertions(+), 43 deletions(-)
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index d004bb0f67e..c1afb4097b5 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -149,8 +149,11 @@ PostgreSQL documentation
of <envar>PGDATA</envar>.
</para>
<para>
- If a tar archive is provided, its WAL segment files must be in
- sequential order; otherwise, an error will be reported.
+ 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 <envar>TMPDIR</envar>
+ environment variable if it is set; otherwise, the temporary files will
+ be created within the same directory as the tar archive itself.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/astreamer_waldump.c b/src/bin/pg_waldump/astreamer_waldump.c
index caf7da6ccb8..40876c77f6c 100644
--- a/src/bin/pg_waldump/astreamer_waldump.c
+++ b/src/bin/pg_waldump/astreamer_waldump.c
@@ -18,8 +18,8 @@
#include "access/xlog_internal.h"
#include "access/xlogdefs.h"
+#include "common/file_perm.h"
#include "common/logging.h"
-#include "fe_utils/simple_list.h"
#include "pg_waldump.h"
/*
@@ -42,10 +42,11 @@ typedef struct astreamer_waldump
/* These fields change with archive member. */
bool skipThisSeg;
+ bool writeThisSeg;
+ FILE *segFp;
XLogSegNo nextSegNo; /* Next expected segment to stream */
} astreamer_waldump;
-static int astreamer_archive_read(XLogDumpPrivate *privateInfo);
static void astreamer_waldump_content(astreamer *streamer,
astreamer_member *member,
const char *data, int len,
@@ -56,7 +57,12 @@ 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,
@@ -164,7 +170,7 @@ astreamer_wal_read(char *readBuff, XLogRecPtr targetPagePtr, Size count,
/*
* Reads the archive and passes it to the archive streamer for decompression.
*/
-static int
+int
astreamer_archive_read(XLogDumpPrivate *privateInfo)
{
int rc;
@@ -208,17 +214,8 @@ astreamer_waldump_new(XLogRecPtr startptr, XLogRecPtr endPtr,
if (XLogRecPtrIsInvalid(startptr))
streamer->startSegNo = 0;
else
- {
XLByteToSeg(startptr, streamer->startSegNo, WalSegSz);
- /*
- * Initialize the record pointer to the beginning of the first
- * segment; this pointer will track the WAL record reading status.
- */
- XLogSegNoOffsetToRecPtr(streamer->startSegNo, 0, WalSegSz,
- privateInfo->archive_streamer_read_ptr);
- }
-
if (XLogRecPtrIsInvalid(endPtr))
streamer->endSegNo = UINT64_MAX;
else
@@ -247,14 +244,16 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
{
case ASTREAMER_MEMBER_HEADER:
{
- XLogSegNo segNo;
+ 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, &segNo))
+ privateInfo->timeline,
+ &fname, &privateInfo->curSegNo))
{
mystreamer->skipThisSeg = true;
break;
@@ -267,33 +266,67 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
if (READ_ANY_WAL(mystreamer))
break;
- /* WAL segments must be archived in order */
- if (mystreamer->nextSegNo != segNo)
+ /*
+ * 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)
{
- pg_log_error("WAL files are not archived in sequential order");
- pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
- mystreamer->nextSegNo, segNo);
- exit(1);
+ mystreamer->writeThisSeg = true;
+ mystreamer->segFp =
+ member_prepare_tmp_write(privateInfo->curSegNo, fname);
+ break;
}
/*
- * We track the reading of WAL segment records using a pointer
- * that's continuously incremented by the length of the
- * received data. This pointer is crucial for serving WAL page
- * requests from the WAL decoding routine, so it must be
- * accurate.
+ * 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.
*/
-#ifdef USE_ASSERT_CHECKING
- if (mystreamer->nextSegNo != 0)
+ else if (privateInfo->archive_streamer_buf->len != 0)
{
XLogRecPtr recPtr;
- XLogSegNoOffsetToRecPtr(segNo, 0, WalSegSz, recPtr);
- Assert(privateInfo->archive_streamer_read_ptr == 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;
+ }
}
-#endif
+
+ 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 += 1;
+ mystreamer->nextSegNo =
+ member_next_segno(privateInfo->curSegNo,
+ privateInfo->timeline);
}
break;
@@ -302,12 +335,45 @@ astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
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:
@@ -334,8 +400,14 @@ astreamer_waldump_finalize(astreamer *streamer)
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);
}
@@ -347,7 +419,8 @@ astreamer_waldump_free(astreamer *streamer)
*/
static bool
member_is_relevant_wal(astreamer_waldump *mystreamer, astreamer_member *member,
- TimeLineID startTimeLineID, XLogSegNo *curSegNo)
+ TimeLineID startTimeLineID, char **curFname,
+ XLogSegNo *curSegNo)
{
int pathlen;
XLogSegNo segNo;
@@ -382,7 +455,84 @@ member_is_relevant_wal(astreamer_waldump *mystreamer, astreamer_member *member,
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/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 393d6bfa9ef..615227b691c 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -43,6 +43,10 @@ static const char *progname;
int WalSegSz = DEFAULT_XLOG_SEG_SIZE;
static volatile sig_atomic_t time_to_stop = false;
+/* Temporary exported WAL file directory and the list */
+char *TmpWalSegDir = NULL;
+SimpleStringList TmpWalSegList = {NULL, NULL};
+
static const RelFileLocator emptyRelFileLocator = {0, 0, 0};
typedef struct XLogDumpConfig
@@ -360,6 +364,41 @@ is_tar_file(const char *fname, pg_compress_algorithm *compression)
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.
*/
@@ -404,6 +443,7 @@ init_tar_archive_reader(XLogDumpPrivate *private, const char *waldir,
streamer = astreamer_zstd_decompressor_new(streamer);
private->archive_streamer = streamer;
+ private->curSegNo = 0;
}
/*
@@ -548,7 +588,7 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPtr, reqLen);
+ int count = required_read_len(private, targetPagePtr, reqLen);
WALReadError errinfo;
if (private->endptr_reached)
@@ -607,12 +647,70 @@ TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
XLogRecPtr targetPtr, char *readBuff)
{
XLogDumpPrivate *private = state->private_data;
- int count = required_read_len(private, targetPtr, reqLen);
+ int count = required_read_len(private, targetPagePtr, reqLen);
+ XLogSegNo nextSegNo;
if (private->endptr_reached)
return -1;
- /* Read the WAL page from the archive streamer */
+ /*
+ * 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);
}
@@ -1340,7 +1438,6 @@ main(int argc, char **argv)
if (fname != NULL && is_tar_file(fname, &compression))
{
private.archive_name = fname;
- waldir = walpath ? pg_strdup(walpath) : pg_strdup(".");
is_tar = true;
}
else
@@ -1434,6 +1531,13 @@ main(int argc, char **argv)
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,
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
index 4205e0ef597..1a1cf35e6f3 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -13,9 +13,14 @@
#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
@@ -31,15 +36,32 @@ typedef struct XLogDumpPrivate
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 */
+ 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);
-extern astreamer *astreamer_waldump_new(XLogRecPtr startptr,
- XLogRecPtr endptr,
+ 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 443126a9ce6..d5fa1f6d28d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -7,6 +7,7 @@ use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+use List::Util qw(shuffle);
my $tar = $ENV{TAR};
@@ -272,7 +273,7 @@ sub generate_archive
}
closedir $dh;
- @files = sort @files;
+ @files = shuffle @files;
# move into the WAL directory before archiving files
my $cwd = getcwd;
--
2.47.1
v4-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchapplication/x-patch; name=v4-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchDownload
From 9c768466e35384d3366abd6ce0d04b6932116256 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 14:47:43 +0530
Subject: [PATCH v4 6/8] pg_verifybackup: Delay default WAL directory
preparation.
We are not sure whether to parse WAL from a directory or an archive
until the backup format is known. Therefore, we delay preparing the
default WAL directory until the point of parsing. This delay is
harmless, as the WAL directory is not used elsewhere.
---
src/bin/pg_verifybackup/pg_verifybackup.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 5e6c13bb921..31ebc1581fb 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -285,10 +285,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
@@ -368,6 +364,10 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
+ /* 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 parse the required ranges of WAL records, unless we were told
* not to do so.
--
2.47.1
v4-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchapplication/x-patch; name=v4-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchDownload
From 43c598c482171e1c5d764ead6614c95104207aa4 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 24 Jul 2025 16:37:43 +0530
Subject: [PATCH v4 7/8] pg_verifybackup: Rename the wal-directory switch to
wal-path
With previous patches to pg_waldump can now decode WAL directly from
tar files. This means you'll be able to specify a tar archive path
instead of a traditional WAL directory.
To keep things consistent and more versatile, we should also
generalize the input switch for pg_verifybackup. It should accept
either a directory or a tar file path that contains WALs. This change
will also aligning it with the existing manifest-path switch naming.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 2 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 22 +++++++++++-----------
src/bin/pg_verifybackup/po/de.po | 4 ++--
src/bin/pg_verifybackup/po/el.po | 4 ++--
src/bin/pg_verifybackup/po/es.po | 4 ++--
src/bin/pg_verifybackup/po/fr.po | 4 ++--
src/bin/pg_verifybackup/po/it.po | 4 ++--
src/bin/pg_verifybackup/po/ja.po | 4 ++--
src/bin/pg_verifybackup/po/ka.po | 4 ++--
src/bin/pg_verifybackup/po/ko.po | 4 ++--
src/bin/pg_verifybackup/po/ru.po | 4 ++--
src/bin/pg_verifybackup/po/sv.po | 4 ++--
src/bin/pg_verifybackup/po/uk.po | 4 ++--
src/bin/pg_verifybackup/po/zh_CN.po | 4 ++--
src/bin/pg_verifybackup/po/zh_TW.po | 4 ++--
src/bin/pg_verifybackup/t/007_wal.pl | 4 ++--
16 files changed, 40 insertions(+), 40 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index 61c12975e4a..e9b8bfd51b1 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -261,7 +261,7 @@ PostgreSQL documentation
<varlistentry>
<term><option>-w <replaceable class="parameter">path</replaceable></option></term>
- <term><option>--wal-directory=<replaceable class="parameter">path</replaceable></option></term>
+ <term><option>--wal-path=<replaceable class="parameter">path</replaceable></option></term>
<listitem>
<para>
Try to parse WAL files stored in the specified directory, rather than
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 31ebc1581fb..1ee400199da 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -93,7 +93,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 +126,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 +135,7 @@ 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 *pg_waldump_path = NULL;
DIR *dir;
@@ -221,8 +221,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 */
@@ -365,15 +365,15 @@ main(int argc, char **argv)
verify_backup_checksums(&context);
/* By default, look for the WAL in the backup directory, too. */
- if (wal_directory == NULL)
- wal_directory = psprintf("%s/pg_wal", context.backup_directory);
+ if (wal_path == NULL)
+ wal_path = psprintf("%s/pg_wal", context.backup_directory);
/*
* 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
@@ -1198,7 +1198,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 +1208,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 +1376,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 a9e24931100..9b5cd5898cf 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 3e3f20c67c5..81442f51c17 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 0cb958f3448..7f729fa35ba 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 da8c72f6427..09937966fa7 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 317b0b71e7f..4da68d0074e 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 c910fb236cc..a948959b54f 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 982751984c7..ef2799316a8 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 acdc3da5e02..eaf91ef1e98 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 64005feedfd..7fb0e5ab1f6 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 17240feeb5c..97125838e8c 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 034b9764232..63f8041ab38 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 b7d97c8976d..fb6fcae8b82 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 c1b710b0a36..568f972b0bb 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/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl
index babc4f0a86b..b07f80719b0 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', '--no-parse-wal', $backup_path ],
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: $!";
--
2.47.1
v4-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchapplication/x-patch; name=v4-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchDownload
From ca9b3ccec8143a53c2dbcae3a11a0edf09f5b96b Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 17 Jul 2025 16:39:36 +0530
Subject: [PATCH v4 8/8] pg_verifybackup: enabled WAL parsing for tar-format
backup
Now that pg_waldump supports decoding from tar archives, we should
leverage this functionality to remove the previous restriction on WAL
parsing for tar-backed formats.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 5 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 66 +++++++++++++------
src/bin/pg_verifybackup/t/002_algorithm.pl | 4 --
src/bin/pg_verifybackup/t/003_corruption.pl | 4 +-
src/bin/pg_verifybackup/t/008_untar.pl | 3 +-
src/bin/pg_verifybackup/t/010_client_untar.pl | 3 +-
6 files changed, 50 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index e9b8bfd51b1..16b50b5a4df 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -36,10 +36,7 @@ PostgreSQL documentation
<literal>backup_manifest</literal> 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 <application>pg_basebackup</application>. However, at present,
- <literal>WAL</literal> verification is supported only for plain-format
- backups. Therefore, if the backup is stored in tar-format, the
- <literal>-n, --no-parse-wal</literal> option should be used.
+ supported by <application>pg_basebackup</application>.
</para>
<para>
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 1ee400199da..4bfe6fdff16 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);
@@ -136,6 +140,8 @@ main(int argc, char **argv)
bool no_parse_wal = false;
bool quiet = false;
char *wal_path = NULL;
+ char *base_archive_path = NULL;
+ char *wal_archive_path = NULL;
char *pg_waldump_path = NULL;
DIR *dir;
@@ -327,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'.
@@ -346,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.
@@ -364,9 +359,28 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
- /* By default, look for the WAL in the backup directory, too. */
+ /*
+ * 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)
- wal_path = psprintf("%s/pg_wal", context.backup_directory);
+ {
+ 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
@@ -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 <tablespaceoid>.tar file here. */
diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl
index ae16c11bc4d..4f284a9e828 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 1dd60f709cf..f1ebdbb46b4 100644
--- a/src/bin/pg_verifybackup/t/003_corruption.pl
+++ b/src/bin/pg_verifybackup/t/003_corruption.pl
@@ -193,10 +193,8 @@ for my $scenario (@scenario)
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/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl
index bc3d6b352ad..0cfe1f9532c 100644
--- a/src/bin/pg_verifybackup/t/008_untar.pl
+++ b/src/bin/pg_verifybackup/t/008_untar.pl
@@ -123,8 +123,7 @@ for my $tc (@test_configuration)
# 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 b62faeb5acf..76269a73673 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 @@ for my $tc (@test_configuration)
# 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");
--
2.47.1
On Thu, Sep 25, 2025 at 4:25 AM Amul Sul <sulamul@gmail.com> wrote:
Another thing that isn't so nice right now is that
verify_tar_archive() has to open and close the archive only for
init_tar_archive_reader() to be called to reopen it again just moments
later. It would be nicer to open the file just once and then keep it
open. Here again, I wonder if the separation of duties could be a bit
cleaner.Prefer to keep those separate, assuming that reopening the file won't
cause any significant harm. Let me know if you think otherwise.
Well, I guess I'd like to know why we can't do better. I'm not really
worried about performance, but reopening the file means that you can
never make it work with reading from a pipe.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mon, Sep 29, 2025 at 8:45 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Sep 25, 2025 at 4:25 AM Amul Sul <sulamul@gmail.com> wrote:
Another thing that isn't so nice right now is that
verify_tar_archive() has to open and close the archive only for
init_tar_archive_reader() to be called to reopen it again just moments
later. It would be nicer to open the file just once and then keep it
open. Here again, I wonder if the separation of duties could be a bit
cleaner.Prefer to keep those separate, assuming that reopening the file won't
cause any significant harm. Let me know if you think otherwise.Well, I guess I'd like to know why we can't do better. I'm not really
worried about performance, but reopening the file means that you can
never make it work with reading from a pipe.
I have some skepticism regarding the extra coding that might be
introduced, as performance is not my primary concern here. If we aim
to keep the file open only once, that logic should be implemented
before calling verify_tar_archive(), not inside it. Implementing the
open and close logic within verify_tar_archive() and
free_tar_archive_reader() would create a confusing and scattered
pattern, especially since these separate operations require only two
lines of code each (open and close if it's a tar file). My second,
concern is that after verify_tar_archive(), we might need to reset the
file reader offset to the beginning. While reusing the buffered data
from the first iteration is technically possible, that only works if
the desired start LSN is at the absolute beginning of the archive, or
later in the sequence, which cannot be reliably guaranteed. Therefore,
for simplicity and avoid the complexity of managing that offset reset
code, I am thinking of a simpler approach.
Regards,
Amul
On Mon, Sep 29, 2025 at 12:17 PM Amul Sul <sulamul@gmail.com> wrote:
While reusing the buffered data
from the first iteration is technically possible, that only works if
the desired start LSN is at the absolute beginning of the archive, or
later in the sequence, which cannot be reliably guaranteed.
I spent a bunch of time studying this code today and I think that the
problem you're talking about here is evidence of a design problem with
astreamer_wal_read() and some of the other code in
astreamer_waldump.c. Your code calls astreamer_wal_read() when it
wants to peek at the first xlog block to determine the WAL segment
size, and it also calls astreamer_wal_read() when it wants read WAL
sequentially beginning at the start LSN and continuing until it
reaches the end LSN. However, these two cases have very different
requirements. verify_tar_archive(), which is misleadingly named and
really exists to determine the WAL segment size, just wants to read
the first xlog block that physically appears in the archive. Every
xlog block will have the same WAL segment size, so it does not matter
which one we read. On the other hand, TarWALDumpReadPage wants to read
WAL in sequential order. In other words, one call to
astreamer_wal_read() really wants to read a block without any block
reordering, and the other call wants to read a block with block
reordering.
To me, it looks like the problem here is that the block reordering
functionality should live on top of the astreamer, not inside of it.
Imagine that astreamer just spits out the bytes in the order in which
they physically appear in the archive, and then there's another
component that consumes and reorders those bytes. So, you read data
and push it into the astreamer until the number of bytes in the output
buffer is at least XLOG_BLCKSZ, and then from there you extract the
WAL segment size. Then, you call XLogReaderAllocate() and enter the
main loop. The reordering logic lives inside of TarWALDumpReadPage().
Each time it gets data from the astreamer's buffer, it either returns
it to the caller if it's in order or buffers it using temporary files
if not.
I found it's actually quite easy to write a patch that avoids
reopening the file. Here it is, on top of your v4:
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 2c42df46d43..c4346a5e211 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -368,17 +368,8 @@ 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.
@@ -1416,12 +1407,22 @@ main(int argc, char **argv)
/* we have everything we need, start reading */
if (is_tar)
{
+ /* Open tar archive and store its file descriptor */
+ private.archive_fd =
+ open_file_in_directory(waldir, private.archive_name);
+ if (private.archive_fd < 0)
+ pg_fatal("could not open file \"%s\"", private.archive_name);
+
/* Verify that the archive contains valid WAL files */
waldir = waldir ? pg_strdup(waldir) : pg_strdup(".");
init_tar_archive_reader(&private, waldir, InvalidXLogRecPtr,
InvalidXLogRecPtr, compression);
verify_tar_archive(&private);
- free_tar_archive_reader(&private);
+ astreamer_free(private.archive_streamer);
+
+ if (lseek(private.archive_fd, 0, SEEK_SET) != 0)
+ pg_log_error("could not seek in file \"%s\": %m",
+ private.archive_name);
/* Set up for reading tar file */
init_tar_archive_reader(&private, waldir, private.startptr,
Of course, this is not really what we want to do: it avoids reopening
the file, but because we can't back up the archive streamer once it's
been created, we have to lseek back to the beginning of the file. But
notice how silly this looks: with this patch, we free the archive
reader and immediately create a new archive reader that is exactly the
same in every way except that we call astreamer_waldump_new(startptr,
endptr, private) instead of astreamer_waldump_new(InvalidXLogRecPtr,
InvalidXLogRecPtr, private). We could arrange to update the original
archive streamer with new values of startSegNo and endSegNo after
verify_tar_archive(), but that's still not quite good enough, because
we might have already made some decisions on what to do with the data
that we read that it's too late to reverse. But, what that means is
that the astreamer_waldump machinery is not smart enough to read one
block of data without making irreversible decisions from which we
can't recover without recreating the entire object. I think we can,
and should, try to do better.
It's also worth noting that the unfortunate layering doesn't just
require us to read the first block of the file: it also complicates
the code in various places. The fact that astreamer_wal_read() needs a
special case for XLogRecPtrIsInvalid(recptr) is a direct result of
this problem, and the READ_ANY_WAL() macro and both the places that
test it are also direct results of this problem. In other words, I'm
arguing that astreamer_wal_read() is incorrectly defined, and that
error creates ugliness in the code both above and below
astreamer_wal_read().
While I'm on the topic of astreamer_wal_read(), here are a few other
problems I noticed:
* The return value is not documented, and it seems to always be count,
in which case it might as well return void. The caller already has the
value they passed for count.
* It seems like it would be more appropriate to assert that endPtr >=
len and just set startPtr = endPtr - len. I don't see how len > endPtr
can ever happen, and I bet bad things will happen if it does.
* "pg_waldump never ask the same" -> "pg_waldump never asks for the same"
Also, this is absolutely not OK with me:
/* Fetch more data */
if (astreamer_archive_read(privateInfo) == 0)
{
char fname[MAXFNAMELEN];
XLogSegNo segno;
XLByteToSeg(targetPagePtr, segno, WalSegSz); an
XLogFileName(fname,
privateInfo->timeline, segno, WalSegSz);
pg_fatal("could not find file \"%s\"
in \"%s\" archive",
fname,
privateInfo->archive_name);
}
astreamer_archive_read() will return 0 if we reach the end of the
tarfile, so this is saying that if we reach the end of the tar file
without finding the range of bytes for which we're looking, the
explanation must be that the relevant WAL file is missing from the
archive. But that is way too much action at a distance. I was able to
easily construct a counterexample by copying the first 81920 bytes of
a valid WAL file and then doing this:
[robert.haas pgsql-meson]$ tar tf pg_wal.tar
000000010000000000000005
[robert.haas pgsql-meson]$ pg_waldump -s 0/050008D8 -e 0/05FFED98
pg_wal.tar >/dev/null
pg_waldump: error: could not find file "000000010000000000000005" in
"pg_wal.tar" archive
Without the redirection to /dev/null, what happened was that
pg_waldump printed out a bunch of records from
000000010000000000000005 and then said that 000000010000000000000005
could not be found, which is obviously silly. But the fact that I
found a specific counterexample here isn't even really the point. The
point is that there's a big gap between what we actually know at this
point (which is that we've read the whole input file) and what the
message is claiming (which is that the reason must be that the file is
missing from the archive). Even if the counterexample above didn't
exist and that really were the only way for that to happen as of
today, that's very fragile. Maybe some future code change will make it
so that there's a second reason that could happen. How would somebody
realize that they had created a second condition by means of which
this code could be reached? If they did realize it, how would they get
the correct error to be reported?
I'm not quite sure how this should be fixed, but I strongly suspect
that the error report here needs to move closer to the code that is
doing the file reordering. Aside from the possibility of the file
being missing and the possibility of the file being too short, a third
possibility is that targetPagePtr retreats between one call and the
next. That really shouldn't happen, but there are no asserts here
verifying that it doesn't.
I also don't like the fact that one call to astreamer_archive_read()
checks the return value (but only whether it's zero, the specific
return value apparently doesn't matter, so why doesn't it return
bool?) and the other doesn't. That kind of coding pattern is very
rarely correct. The code says:
/* 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);
}
But it's unclear why this should be good enough to ensure that enough
data is available. astreamer_archive_read() might read zero bytes and
return 0, so this doesn't really guarantee anything at all. On the
other hand, even if astereamer_archive_read() returns a non-zero
value, it's only going to read READ_CHUNK_SIZE bytes from the
underlying file, so if more than that needs to be read in order for us
to have enough data, we won't. I think it's very hard to imagine a
situation in which you can call astreamer_archive_read() without using
some loop. That's what astreamer_wal_read() does: it calls
astreamer_archive_read() until it either returns 0 -- in which case we
know we've failed -- or until we have enough data. Here we just hope
that calling it once is enough, and that checking for errors is
unimportant. I also don't understand the reference to a race
condition, because there's only one process with one thread here, I
believe, so what would be racing against?
Another thing I noticed is that astreamer_archive_read() makes
reference to decrypting, but there's no cryptography involved in any
of this.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Oct 10, 2025 at 11:32 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Sep 29, 2025 at 12:17 PM Amul Sul <sulamul@gmail.com> wrote:
While reusing the buffered data
from the first iteration is technically possible, that only works if
the desired start LSN is at the absolute beginning of the archive, or
later in the sequence, which cannot be reliably guaranteed.I spent a bunch of time studying this code today and I think that the
problem you're talking about here is evidence of a design problem with
astreamer_wal_read() and some of the other code in
astreamer_waldump.c. Your code calls astreamer_wal_read() when it
wants to peek at the first xlog block to determine the WAL segment
size, and it also calls astreamer_wal_read() when it wants read WAL
sequentially beginning at the start LSN and continuing until it
reaches the end LSN. However, these two cases have very different
requirements. verify_tar_archive(), which is misleadingly named and
really exists to determine the WAL segment size, just wants to read
the first xlog block that physically appears in the archive. Every
xlog block will have the same WAL segment size, so it does not matter
which one we read. On the other hand, TarWALDumpReadPage wants to read
WAL in sequential order. In other words, one call to
astreamer_wal_read() really wants to read a block without any block
reordering, and the other call wants to read a block with block
reordering.To me, it looks like the problem here is that the block reordering
functionality should live on top of the astreamer, not inside of it.
Imagine that astreamer just spits out the bytes in the order in which
they physically appear in the archive, and then there's another
component that consumes and reorders those bytes. So, you read data
and push it into the astreamer until the number of bytes in the output
buffer is at least XLOG_BLCKSZ, and then from there you extract the
WAL segment size. Then, you call XLogReaderAllocate() and enter the
main loop. The reordering logic lives inside of TarWALDumpReadPage().
Each time it gets data from the astreamer's buffer, it either returns
it to the caller if it's in order or buffers it using temporary files
if not.
I initially considered implementing the reordering logic outside of
astreamer when we first discussed this project, but the implementation
could get complicated -- or at least feel hacky. Let me explain why:
astreamer reads the archive in fixed-size chunks (here it is 128KB).
Sometimes, a single read can contain data from two WAL files --
specifically, the tail end of one file and the start of the next --
because of how they’re physically stored in the archive. astreamer
knows where one file ends and another begins through tags like
ASTREAMER_MEMBER_HEADER, ASTREAMER_MEMBER_CONTENTS, and
ASTREAMER_MEMBER_TRAILER. However, it can’t pause mid-chunk to hold
data from the next file once the previous one ends and for the caller;
it pushes the entire chunk it has read to the target buffer.
So, if we put the reordering logic outside the streamer, we’d
sometimes be receiving buffers containing mixed data from two WAL
files. The caller would then need to correctly identify WAL file
boundaries within those buffers. This would require passing extra
metadata -- like segment numbers for the WAL files in the buffer, plus
start and end offsets of those segments within the buffer. While not
impossible, it feels a bit hacky and I'm unsure if that’s the best
approach.
I found it's actually quite easy to write a patch that avoids
reopening the file. Here it is, on top of your v4:diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c index 2c42df46d43..c4346a5e211 100644 --- a/src/bin/pg_waldump/pg_waldump.c +++ b/src/bin/pg_waldump/pg_waldump.c @@ -368,17 +368,8 @@ 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. @@ -1416,12 +1407,22 @@ main(int argc, char **argv) /* we have everything we need, start reading */ if (is_tar) { + /* Open tar archive and store its file descriptor */ + private.archive_fd = + open_file_in_directory(waldir, private.archive_name); + if (private.archive_fd < 0) + pg_fatal("could not open file \"%s\"", private.archive_name); + /* Verify that the archive contains valid WAL files */ waldir = waldir ? pg_strdup(waldir) : pg_strdup("."); init_tar_archive_reader(&private, waldir, InvalidXLogRecPtr, InvalidXLogRecPtr, compression); verify_tar_archive(&private); - free_tar_archive_reader(&private); + astreamer_free(private.archive_streamer); + + if (lseek(private.archive_fd, 0, SEEK_SET) != 0) + pg_log_error("could not seek in file \"%s\": %m", + private.archive_name);/* Set up for reading tar file */
init_tar_archive_reader(&private, waldir, private.startptr,Of course, this is not really what we want to do: it avoids reopening
the file, but because we can't back up the archive streamer once it's
been created, we have to lseek back to the beginning of the file. But
notice how silly this looks: with this patch, we free the archive
reader and immediately create a new archive reader that is exactly the
same in every way except that we call astreamer_waldump_new(startptr,
endptr, private) instead of astreamer_waldump_new(InvalidXLogRecPtr,
InvalidXLogRecPtr, private). We could arrange to update the original
archive streamer with new values of startSegNo and endSegNo after
verify_tar_archive(), but that's still not quite good enough, because
we might have already made some decisions on what to do with the data
that we read that it's too late to reverse. But, what that means is
that the astreamer_waldump machinery is not smart enough to read one
block of data without making irreversible decisions from which we
can't recover without recreating the entire object. I think we can,
and should, try to do better.
Agreed.
It's also worth noting that the unfortunate layering doesn't just
require us to read the first block of the file: it also complicates
the code in various places. The fact that astreamer_wal_read() needs a
special case for XLogRecPtrIsInvalid(recptr) is a direct result of
this problem, and the READ_ANY_WAL() macro and both the places that
test it are also direct results of this problem. In other words, I'm
arguing that astreamer_wal_read() is incorrectly defined, and that
error creates ugliness in the code both above and below
astreamer_wal_read().While I'm on the topic of astreamer_wal_read(), here are a few other
problems I noticed:* The return value is not documented, and it seems to always be count,
in which case it might as well return void. The caller already has the
value they passed for count.
The caller will be xlogreader, and I believe we shouldn't change that.
For the same reason, WALDumpReadPage() also returns the same.
* It seems like it would be more appropriate to assert that endPtr >=
len and just set startPtr = endPtr - len. I don't see how len > endPtr
can ever happen, and I bet bad things will happen if it does.
* "pg_waldump never ask the same" -> "pg_waldump never asks for the same"
Ok.
Also, this is absolutely not OK with me:
/* Fetch more data */
if (astreamer_archive_read(privateInfo) == 0)
{
char fname[MAXFNAMELEN];
XLogSegNo segno;XLByteToSeg(targetPagePtr, segno, WalSegSz); an
XLogFileName(fname,
privateInfo->timeline, segno, WalSegSz);pg_fatal("could not find file \"%s\"
in \"%s\" archive",
fname,
privateInfo->archive_name);
}astreamer_archive_read() will return 0 if we reach the end of the
tarfile, so this is saying that if we reach the end of the tar file
without finding the range of bytes for which we're looking, the
explanation must be that the relevant WAL file is missing from the
archive. But that is way too much action at a distance. I was able to
easily construct a counterexample by copying the first 81920 bytes of
a valid WAL file and then doing this:[robert.haas pgsql-meson]$ tar tf pg_wal.tar
000000010000000000000005
[robert.haas pgsql-meson]$ pg_waldump -s 0/050008D8 -e 0/05FFED98
pg_wal.tar >/dev/null
pg_waldump: error: could not find file "000000010000000000000005" in
"pg_wal.tar" archiveWithout the redirection to /dev/null, what happened was that
pg_waldump printed out a bunch of records from
000000010000000000000005 and then said that 000000010000000000000005
could not be found, which is obviously silly. But the fact that I
found a specific counterexample here isn't even really the point. The
point is that there's a big gap between what we actually know at this
point (which is that we've read the whole input file) and what the
message is claiming (which is that the reason must be that the file is
missing from the archive). Even if the counterexample above didn't
exist and that really were the only way for that to happen as of
today, that's very fragile. Maybe some future code change will make it
so that there's a second reason that could happen. How would somebody
realize that they had created a second condition by means of which
this code could be reached? If they did realize it, how would they get
the correct error to be reported?
Agreed, I'll think about this.
/* 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);
}But it's unclear why this should be good enough to ensure that enough
data is available. astreamer_archive_read() might read zero bytes and
return 0, so this doesn't really guarantee anything at all. On the
other hand, even if astereamer_archive_read() returns a non-zero
value, it's only going to read READ_CHUNK_SIZE bytes from the
underlying file, so if more than that needs to be read in order for us
to have enough data, we won't. I think it's very hard to imagine a
situation in which you can call astreamer_archive_read() without using
some loop.
The loop isn't needed because the caller always requests 8KB of data,
while READ_CHUNK_SIZE is 128KB. It’s assumed that the astreamer has
already created the file with some initial data. For example, if only
a few bytes have been written so far, when we reach
TarWALDumpReadPage(), it detects that we’re reading the same file
that the astreamer is still writing to and hasn’t finished. It then request to
appends 128KB of data by calling astreamer_archive_read, even though we
only need 8KB at a time. This process repeats each time the next 8KBchunk is
requested: astreamer_archive_read() appends another 128KB,and continues until
the file has been fully read and written.
That's what astreamer_wal_read() does: it calls
astreamer_archive_read() until it either returns 0 -- in which case we
know we've failed -- or until we have enough data. Here we just hope
that calling it once is enough, and that checking for errors is
unimportant. I also don't understand the reference to a race
condition, because there's only one process with one thread here, I
believe, so what would be racing against?
In the case where the astreamer is exporting a file to disk but hasn’t
finished writing it, and we call TarWALDumpReadPage() to request
block(s) from that WAL file, we can read only up to the existing
blocks in the file. Since the file is incomplete, reading may fail
later. To handle this, astreamer_archive_read() is invoked to append
more data -- usually more than the requested amount, as explained
earlier. That is the race condition I am trying to handle.
Now, regarding the concern of astreamer_archive_read() returning zero
without reading or appending any data: this can happen only if the WAL
is shorter than expected -- an incomplete. In that case,
WALDumpReadPage() will raise the appropriate error, we don't have to
check at that point, I think.
Another thing I noticed is that astreamer_archive_read() makes
reference to decrypting, but there's no cryptography involved in any
of this.
I think that was a typo -- I meant decompression.
Regards,
Amul
On Thu, Oct 16, 2025 at 7:49 AM Amul Sul <sulamul@gmail.com> wrote:
astreamer reads the archive in fixed-size chunks (here it is 128KB).
Sometimes, a single read can contain data from two WAL files --
specifically, the tail end of one file and the start of the next --
because of how they’re physically stored in the archive. astreamer
knows where one file ends and another begins through tags like
ASTREAMER_MEMBER_HEADER, ASTREAMER_MEMBER_CONTENTS, and
ASTREAMER_MEMBER_TRAILER. However, it can’t pause mid-chunk to hold
data from the next file once the previous one ends and for the caller;
it pushes the entire chunk it has read to the target buffer.
Right, this makes sense.
So, if we put the reordering logic outside the streamer, we’d
sometimes be receiving buffers containing mixed data from two WAL
files. The caller would then need to correctly identify WAL file
boundaries within those buffers. This would require passing extra
metadata -- like segment numbers for the WAL files in the buffer, plus
start and end offsets of those segments within the buffer. While not
impossible, it feels a bit hacky and I'm unsure if that’s the best
approach.
I agree that we need that kind of metadata, but I don't see why our
need for it depends on where we do the reordering. That is, if we do
the reordering above the astreamer layer, we need to keep track of the
origin of each chunk of WAL bytes, and if we do the reordering within
the astreamer layer, we still need to keep track of the origin of the
WAL bytes. Doing the ordering properly requires that tracking, but it
doesn't say anything about where that tracking has to be performed.
I think it might be better if we didn't write to the astreamer's
buffer at all. For example, suppose we create a struct that looks
approximately like this:
struct ChunkOfDecodedWAL
{
XLogSegNo segno; // could also be XLogRecPtr start_lsn or char
*walfilename or whatever
StringInfoData buffer;
char *spillfilename; // or whatever we use to identify the temporary files
bool already_removed;
// potentially other metadata
};
Then, create a hash table and key it on the segno whatever. Have the
astreamer write to the hash table: when it gets a chunk of WAL, it
looks up or creates the relevant hash table entry and appends the data
to the buffer. At any convenient point in the code, you can decide to
write the data from the buffer to a spill file, after which you
resetStringInfo() on the buffer and populate the spill file name. When
you've used up the data, you remove the spill file and set the
already_removed flag.
I think this could also help with the error reporting stuff. When you
get to the end of the file, you'll know all the files you saw and how
much data you read from each of them. So you could possibly do
something like
ERROR: LSN %08X/%08X not found in archive "\%s\"
DETAIL: WAL segment %s is not present in the archive
-or
DETAIL: WAL segment %s was expected to be %u bytes, but was only %u bytes
-or-
DETAIL: whatever else can go wrong
The point is that every file you've ever seen has a hash table entry,
and in that hash table entry you can store everything about that file
that you need to know, whether that's the file data, the disk file
that contains the file data, the fact that we already threw the data
away, or any other fact that you can imagine wanting to know.
Said differently, the astreamer buffer is not really a great place to
write data. It exists because when we're just forwarding data from one
astreamer to the next, we will often need to buffer a small amount of
data to avoid terrible performance. However, it's only there to be
used when we don't have something better. I don't think any astreamer
that is intended to be the last one in the chain currently writes to
the buffer -- they write to the output file, or whatever, because
using an in-memory buffer as your final output destination is not a
real good plan.
While I'm on the topic of astreamer_wal_read(), here are a few other
problems I noticed:* The return value is not documented, and it seems to always be count,
in which case it might as well return void. The caller already has the
value they passed for count.The caller will be xlogreader, and I believe we shouldn't change that.
For the same reason, WALDumpReadPage() also returns the same.
OK, but then you can make that clear via a brief comment.
The loop isn't needed because the caller always requests 8KB of data,
while READ_CHUNK_SIZE is 128KB. It’s assumed that the astreamer has
already created the file with some initial data. For example, if only
a few bytes have been written so far, when we reach
TarWALDumpReadPage(), it detects that we’re reading the same file
that the astreamer is still writing to and hasn’t finished. It then request to
appends 128KB of data by calling astreamer_archive_read, even though we
only need 8KB at a time. This process repeats each time the next 8KBchunk is
requested: astreamer_archive_read() appends another 128KB,and continues until
the file has been fully read and written.
Sure, but you don't know how much data is going to come out the other
end of the astreamer pipeline. Since the data is (possibly)
compressed, you expect at least as many bytes to emerge from the
output end as you add to the input end, but it's not a good idea to
rely on assumptions like that. Sometimes compressors end up making the
data slightly larger instead of smaller. It's unlikely that the effect
would be so dramatic that adding 128kB to one end of the pipeline
would make less than 8kB emerge from the other end, but it's not a
good idea to rely on assumptions like that. Not that this is a real
thing, but imagine that the compressed file had something in the
middle of it that behaved like a comment in C code, i.e. it didn't
generate any output.
In the case where the astreamer is exporting a file to disk but hasn’t
finished writing it, and we call TarWALDumpReadPage() to request
block(s) from that WAL file, we can read only up to the existing
blocks in the file. Since the file is incomplete, reading may fail
later. To handle this, astreamer_archive_read() is invoked to append
more data -- usually more than the requested amount, as explained
earlier. That is the race condition I am trying to handle.
That's not what a race condition is:
https://en.wikipedia.org/wiki/Race_condition
Now, regarding the concern of astreamer_archive_read() returning zero
without reading or appending any data: this can happen only if the WAL
is shorter than expected -- an incomplete. In that case,
WALDumpReadPage() will raise the appropriate error, we don't have to
check at that point, I think.
I'm not going to accept that kind of justification -- it is too
fragile to assume that you don't need to check for an error because it
"can't happen". Sometimes that is reasonable, but there is quite a lot
of action-at-a-distance here, so it does not feel safe.
Another thing I noticed is that astreamer_archive_read() makes
reference to decrypting, but there's no cryptography involved in any
of this.I think that was a typo -- I meant decompression.
I figured as much, but it still needs fixing.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mon, Oct 20, 2025 at 8:05 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Oct 16, 2025 at 7:49 AM Amul Sul <sulamul@gmail.com> wrote:
So, if we put the reordering logic outside the streamer, we’d
sometimes be receiving buffers containing mixed data from two WAL
files. The caller would then need to correctly identify WAL file
boundaries within those buffers. This would require passing extra
metadata -- like segment numbers for the WAL files in the buffer, plus
start and end offsets of those segments within the buffer. While not
impossible, it feels a bit hacky and I'm unsure if that’s the best
approach.I agree that we need that kind of metadata, but I don't see why our
need for it depends on where we do the reordering. That is, if we do
the reordering above the astreamer layer, we need to keep track of the
origin of each chunk of WAL bytes, and if we do the reordering within
the astreamer layer, we still need to keep track of the origin of the
WAL bytes. Doing the ordering properly requires that tracking, but it
doesn't say anything about where that tracking has to be performed.I think it might be better if we didn't write to the astreamer's
buffer at all. For example, suppose we create a struct that looks
approximately like this:struct ChunkOfDecodedWAL
{
XLogSegNo segno; // could also be XLogRecPtr start_lsn or char
*walfilename or whatever
StringInfoData buffer;
char *spillfilename; // or whatever we use to identify the temporary files
bool already_removed;
// potentially other metadata
};Then, create a hash table and key it on the segno whatever. Have the
astreamer write to the hash table: when it gets a chunk of WAL, it
looks up or creates the relevant hash table entry and appends the data
to the buffer. At any convenient point in the code, you can decide to
write the data from the buffer to a spill file, after which you
resetStringInfo() on the buffer and populate the spill file name. When
you've used up the data, you remove the spill file and set the
already_removed flag.I think this could also help with the error reporting stuff. When you
get to the end of the file, you'll know all the files you saw and how
much data you read from each of them. So you could possibly do
something likeERROR: LSN %08X/%08X not found in archive "\%s\"
DETAIL: WAL segment %s is not present in the archive
-or
DETAIL: WAL segment %s was expected to be %u bytes, but was only %u bytes
-or-
DETAIL: whatever else can go wrongThe point is that every file you've ever seen has a hash table entry,
and in that hash table entry you can store everything about that file
that you need to know, whether that's the file data, the disk file
that contains the file data, the fact that we already threw the data
away, or any other fact that you can imagine wanting to know.Said differently, the astreamer buffer is not really a great place to
write data. It exists because when we're just forwarding data from one
astreamer to the next, we will often need to buffer a small amount of
data to avoid terrible performance. However, it's only there to be
used when we don't have something better. I don't think any astreamer
that is intended to be the last one in the chain currently writes to
the buffer -- they write to the output file, or whatever, because
using an in-memory buffer as your final output destination is not a
real good plan.
Make sense, I implemented this approach in the attached version, but
with a different structure name and a slightly different error
message. In the error output using the WAL file name instead of the
LSN. This is because the LSN at that point may differ from the
user-provided one (it might have been adjusted to the start of a WAL
page by xlogreader). This follows the same style used in the routine
that reads the WAL file. The LSN values (user provided) are only used
in error messages generated at the very beginning, specifically in the
main() function of pg_waldump.
I have also restructured the code by moving most of the tar file
reading logic out of pg_waldump.c into astreamer_waldump.c, which has
now been renamed to archive_waldump.c.
Kindly have a look at the attached version. Thank you !
Regards,
Amul
Attachments:
v5-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchapplication/x-patch; name=v5-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchDownload
From 9bfed15797bcecf15e828d2b48f64caead36e9bb Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 24 Jun 2025 11:33:20 +0530
Subject: [PATCH v5 1/8] Refactor: pg_waldump: Move some declarations to new
pg_waldump.h
This change prepares for a second source file in this directory to
support reading WAL from tar files. Common structures, declarations,
and functions are being exported through this include file so
they can be used in both files.
---
src/bin/pg_waldump/pg_waldump.c | 11 ++---------
src/bin/pg_waldump/pg_waldump.h | 27 +++++++++++++++++++++++++++
2 files changed, 29 insertions(+), 9 deletions(-)
create mode 100644 src/bin/pg_waldump/pg_waldump.h
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 13d3ec2f5be..a49b2fd96c7 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,19 +40,11 @@
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};
-typedef struct XLogDumpPrivate
-{
- TimeLineID timeline;
- XLogRecPtr startptr;
- XLogRecPtr endptr;
- bool endptr_reached;
-} XLogDumpPrivate;
-
typedef struct XLogDumpConfig
{
/* display options */
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
new file mode 100644
index 00000000000..9e62b64ead5
--- /dev/null
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -0,0 +1,27 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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"
+
+extern int WalSegSz;
+
+/* Contains the necessary information to drive WAL decoding */
+typedef struct XLogDumpPrivate
+{
+ TimeLineID timeline;
+ XLogRecPtr startptr;
+ XLogRecPtr endptr;
+ bool endptr_reached;
+} XLogDumpPrivate;
+
+#endif /* end of PG_WALDUMP_H */
--
2.47.1
v5-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchapplication/x-patch; name=v5-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchDownload
From 830dcb9c9f98de3bfc6d0b19d56865ed1e175860 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 26 Jun 2025 11:42:53 +0530
Subject: [PATCH v5 2/8] Refactor: pg_waldump: Separate logic used to calculate
the required read size.
This refactoring prepares the codebase for an upcoming patch that will
support reading WAL from tar files. The logic for calculating the
required read size has been updated to handle both normal WAL files
and WAL files located inside a tar archive.
---
src/bin/pg_waldump/pg_waldump.c | 39 ++++++++++++++++++++++-----------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index a49b2fd96c7..8d0cd9e7156 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -326,6 +326,29 @@ identify_target_directory(char *directory, char *fname)
return NULL; /* not reached */
}
+/* 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,
@@ -383,21 +406,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))
--
2.47.1
v5-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchapplication/x-patch; name=v5-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchDownload
From fdf23c243bc21cefd062d2b4960460722805bbee Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 30 Jul 2025 12:43:30 +0530
Subject: [PATCH v5 3/8] Refactor: pg_waldump: Restructure TAP tests.
Restructured some tests to run inside a loop, facilitating their
re-execution for decoding WAL from tar archives.
---
src/bin/pg_waldump/t/001_basic.pl | 123 ++++++++++++++++--------------
1 file changed, 67 insertions(+), 56 deletions(-)
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index f26d75e01cf..1b712e8d74d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -198,28 +198,6 @@ command_like(
],
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 +205,6 @@ command_like(
],
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.
@@ -272,7 +241,6 @@ sub test_pg_waldump
my $result = IPC::Run::run [
'pg_waldump',
- '--path' => $node->data_dir,
'--start' => $start_lsn,
'--end' => $end_lsn,
@opts
@@ -288,38 +256,81 @@ sub test_pg_waldump
my @lines;
-@lines = test_pg_waldump;
-is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
+my @scenario = (
+ {
+ 'path' => $node->data_dir
+ });
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenario)
+{
+ my $path = $scenario->{'path'};
-@lines = test_pg_waldump('--fullpage');
-is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
+ SKIP:
+ {
+ 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');
-@lines = test_pg_waldump('--stats');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+ command_fails_like(
+ [
+ 'pg_waldump', '--quiet',
+ '--path' => $path,
+ '--start' => $start_lsn
+ ],
+ qr/error: error in WAL record at/,
+ 'errors are shown with --quiet');
-@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('--path' => $path);
+ is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
-@lines = test_pg_waldump('--rmgr' => 'Btree');
-is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+ @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ is(@lines, 6, 'limit option observed');
-@lines = test_pg_waldump('--fork' => 'init');
-is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+ @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
-@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('--path' => $path, '--stats');
+ like($lines[0], qr/WAL statistics/, "statistics on stdout");
+ is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
-@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');
+ @lines = test_pg_waldump('--path' => $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' => $path, '--rmgr' => 'Btree');
+ is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+
+ @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+
+ @lines = test_pg_waldump('--path' => $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' => $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');
+ }
+}
done_testing();
--
2.47.1
v5-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchapplication/x-patch; name=v5-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchDownload
From 787fc3c94431dedcc0d37d3d6d9329b62e4d00c5 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 5 Nov 2025 15:40:36 +0530
Subject: [PATCH v5 4/8] pg_waldump: Add support for archived WAL decoding.
pg_waldump can now accept the path to a tar archive containing WAL
files and decode them. This feature was added primarily for
pg_verifybackup, which previously disabled WAL parsing for
tar-formatted backups.
Note that this patch requires that the WAL files within the archive be
in sequential order; an error will be reported otherwise. The next
patch is planned to remove this restriction.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/Makefile | 7 +-
src/bin/pg_waldump/archive_waldump.c | 577 +++++++++++++++++++++++++++
src/bin/pg_waldump/meson.build | 4 +-
src/bin/pg_waldump/pg_waldump.c | 222 ++++++++---
src/bin/pg_waldump/pg_waldump.h | 36 +-
src/bin/pg_waldump/t/001_basic.pl | 84 +++-
src/tools/pgindent/typedefs.list | 3 +
8 files changed, 863 insertions(+), 78 deletions(-)
create mode 100644 src/bin/pg_waldump/archive_waldump.c
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index ce23add5577..d004bb0f67e 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -141,13 +141,17 @@ PostgreSQL documentation
<term><option>--path=<replaceable>path</replaceable></option></term>
<listitem>
<para>
- Specifies a directory to search for WAL segment files or a
- directory with a <literal>pg_wal</literal> subdirectory that
+ Specifies a tar archive or a directory to search for WAL segment files
+ or a directory with a <literal>pg_wal</literal> subdirectory that
contains such files. The default is to search in the current
directory, the <literal>pg_wal</literal> subdirectory of the
current directory, and the <literal>pg_wal</literal> subdirectory
of <envar>PGDATA</envar>.
</para>
+ <para>
+ If a tar archive is provided, its WAL segment files must be in
+ sequential order; otherwise, an error will be reported.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile
index 4c1ee649501..05ac5763a57 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 \
+ archive_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/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
new file mode 100644
index 00000000000..e619e29d5d4
--- /dev/null
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -0,0 +1,577 @@
+/*-------------------------------------------------------------------------
+ *
+ * archive_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/archive_waldump.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <unistd.h>
+
+#include "access/xlog_internal.h"
+#include "common/hashfn.h"
+#include "common/logging.h"
+#include "fe_utils/simple_list.h"
+#include "pg_waldump.h"
+
+/*
+ * How many bytes should we try to read from a file at once?
+ */
+#define READ_CHUNK_SIZE (128 * 1024)
+
+/* Structure for storing the WAL segment data from the archive */
+typedef struct ArchivedWALEntry
+{
+ uint32 status; /* hash status */
+ XLogSegNo segno; /* hash key: WAL segment number */
+ TimeLineID timeline; /* timeline of this wal file */
+
+ StringInfoData buf;
+ bool tmpseg_exists; /* spill file exists? */
+
+ int total_read; /* total read of this WAL segment, including
+ * buffered and temporarily written data */
+} ArchivedWALEntry;
+
+#define SH_PREFIX ArchivedWAL
+#define SH_ELEMENT_TYPE ArchivedWALEntry
+#define SH_KEY_TYPE XLogSegNo
+#define SH_KEY segno
+#define SH_HASH_KEY(tb, key) murmurhash64((uint64) key)
+#define SH_EQUAL(tb, a, b) (a == b)
+#define SH_GET_HASH(tb, a) a->hash
+#define SH_SCOPE static inline
+#define SH_RAW_ALLOCATOR pg_malloc0
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
+
+static ArchivedWAL_hash *ArchivedWAL_HTAB = NULL;
+
+typedef struct astreamer_waldump
+{
+ astreamer base;
+ XLogDumpPrivate *privateInfo;
+} astreamer_waldump;
+
+static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
+static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
+ XLogDumpPrivate *privateInfo);
+
+static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
+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_wal_file(astreamer_waldump *mystreamer,
+ astreamer_member *member,
+ XLogSegNo *curSegNo,
+ TimeLineID *curTimeline);
+
+static const astreamer_ops astreamer_waldump_ops = {
+ .content = astreamer_waldump_content,
+ .finalize = astreamer_waldump_finalize,
+ .free = astreamer_waldump_free
+};
+
+/*
+ * Returns true if the given file is a tar archive and outputs its compression
+ * algorithm.
+ */
+bool
+is_archive_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;
+}
+
+/*
+ * Initializes the tar archive reader to read WAL files from the archive,
+ * creates a hash table to store them, performs quick existence checks for WAL
+ * entries in the archive and retrieves the WAL segment size, and sets up
+ * filtering criteria for relevant entries.
+ */
+void
+init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
+ pg_compress_algorithm compression)
+{
+ int fd;
+ astreamer *streamer;
+ ArchivedWALEntry *entry = NULL;
+ XLogLongPageHeader longhdr;
+
+ /* Open tar archive and store its file descriptor */
+ fd = open_file_in_directory(waldir, privateInfo->archive_name);
+
+ if (fd < 0)
+ pg_fatal("could not open file \"%s\"", privateInfo->archive_name);
+
+ privateInfo->archive_fd = fd;
+
+ streamer = astreamer_waldump_new(privateInfo);
+
+ /* 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);
+
+ privateInfo->archive_streamer = streamer;
+
+ /* Hash table storing WAL entries read from the archive */
+ ArchivedWAL_HTAB = ArchivedWAL_create(16, NULL);
+
+ /*
+ * Verify that the archive contains valid WAL files and fetch WAL segment
+ * size
+ */
+ while (entry == NULL || entry->buf.len < XLOG_BLCKSZ)
+ {
+ if (read_archive_file(privateInfo, XLOG_BLCKSZ) == 0)
+ pg_fatal("could not find WAL in \"%s\" archive",
+ privateInfo->archive_name);
+
+ entry = privateInfo->cur_wal;
+ }
+
+ /* Set WalSegSz if WAL data is successfully read */
+ longhdr = (XLogLongPageHeader) entry->buf.data;
+
+ WalSegSz = longhdr->xlp_seg_size;
+
+ if (!IsValidWalSegSize(WalSegSz))
+ {
+ pg_log_error(ngettext("invalid WAL segment size in WAL file from archive \"%s\" (%d byte)",
+ "invalid WAL segment size in WAL file from archive \"%s\" (%d bytes)",
+ WalSegSz),
+ privateInfo->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);
+ }
+
+ /*
+ * With the WAL segment size available, we can now initialize the
+ * dependent start and end segment numbers.
+ */
+ XLByteToSeg(privateInfo->startptr, privateInfo->startSegNo, WalSegSz);
+ XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+}
+
+/*
+ * Release the archive streamer chain and close the archive file.
+ */
+void
+free_archive_reader(XLogDumpPrivate *privateInfo)
+{
+ /*
+ * 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(privateInfo->archive_streamer);
+
+ /* Close the file. */
+ if (close(privateInfo->archive_fd) != 0)
+ pg_log_error("could not close file \"%s\": %m",
+ privateInfo->archive_name);
+}
+
+/*
+ * Copies WAL data from astreamer to readBuff; if unavailable, fetches more
+ * from the tar archive via astreamer.
+ */
+int
+read_archive_wal_page(XLogDumpPrivate *privateInfo, XLogRecPtr targetPagePtr,
+ Size count, char *readBuff)
+{
+ char *p = readBuff;
+ Size nbytes = count;
+ XLogRecPtr recptr = targetPagePtr;
+ XLogSegNo segno;
+ ArchivedWALEntry *entry;
+
+ XLByteToSeg(targetPagePtr, segno, WalSegSz);
+ entry = get_archive_wal_entry(segno, privateInfo);
+
+ while (nbytes > 0)
+ {
+ char *buf = entry->buf.data;
+ int len = entry->buf.len;
+
+ /* WAL record range that the buffer contains */
+ XLogRecPtr endPtr;
+ XLogRecPtr startPtr;
+
+ XLogSegNoOffsetToRecPtr(entry->segno, entry->total_read,
+ WalSegSz, endPtr);
+ startPtr = endPtr - len;
+
+ Assert((endPtr - startPtr) == len);
+
+ /*
+ * 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(&entry->buf);
+ }
+
+ if (len > 0 && recptr > startPtr)
+ {
+ int skipBytes = 0;
+
+ /*
+ * The required offset is not at the start of the 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 reading correct WAL record */
+ Assert(recptr >= startPtr && recptr < endPtr);
+
+ memcpy(p, buf, readBytes);
+
+ /* Update state for read */
+ nbytes -= readBytes;
+ p += readBytes;
+ recptr += readBytes;
+ }
+ else
+ {
+ /*
+ * Fetch more data; raise an error if it's not the current segment
+ * being read by the archive streamer or if reading of the
+ * archived file has finished.
+ */
+ if (privateInfo->cur_wal != entry ||
+ read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ {
+ char fname[MAXFNAMELEN];
+
+ XLogFileName(fname, privateInfo->timeline, entry->segno,
+ WalSegSz);
+ pg_fatal("could not read file \"%s\" from archive \"%s\": read %lld of %lld",
+ fname, privateInfo->archive_name,
+ (long long int) count - nbytes,
+ (long long int) nbytes);
+ }
+ }
+ }
+
+ /*
+ * Should have either have successfully read all the requested bytes or
+ * reported a failure before this point.
+ */
+ Assert(nbytes == 0);
+
+ /*
+ * NB: We return the fixed value provided as input. Although we could
+ * return a boolean since we either successfully read the WAL page or
+ * raise an error, but the caller expects this value to be returned. The
+ * routine that reads WAL pages from the physical WAL file follows the
+ * same convention.
+ */
+ return count;
+}
+
+/*
+ * Reads the archive file and passes it to the archive streamer for
+ * decompression.
+ */
+static int
+read_archive_file(XLogDumpPrivate *privateInfo, Size count)
+{
+ int rc;
+ char *buffer;
+
+ buffer = pg_malloc(READ_CHUNK_SIZE * sizeof(uint8));
+
+ rc = read(privateInfo->archive_fd, buffer, count);
+ if (rc < 0)
+ pg_fatal("could not read file \"%s\": %m",
+ privateInfo->archive_name);
+
+ /*
+ * Decompress (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;
+}
+
+/*
+ * Returns the archived WAL entry from the hash table if it exists. Otherwise,
+ * it invokes the routine to read the archived file and retrieve the entry if
+ * it is not already in hash table.
+ */
+static ArchivedWALEntry *
+get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
+{
+ ArchivedWALEntry *entry = NULL;
+ char fname[MAXFNAMELEN];
+
+ /* Search hash table */
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry != NULL)
+ return entry;
+
+ /* Needed WAL yet to be decoded from archive, do the same */
+ while (1)
+ {
+ entry = privateInfo->cur_wal;
+
+ /* Fetch more data */
+ if (entry == NULL || entry->buf.len == 0)
+ {
+ if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ break; /* archive file ended */
+ }
+
+ /*
+ * Either, here for the first time, or the archived streamer is
+ * reading a non-WAL file or an irrelevant WAL file.
+ */
+ if (entry == NULL)
+ continue;
+
+ /* Found the required entry */
+ if (entry->segno == segno)
+ return entry;
+
+ /*
+ * Ignore if the timeline is different or the current segment is not
+ * the desired one.
+ */
+ if (privateInfo->timeline != entry->timeline ||
+ privateInfo->startSegNo > entry->segno ||
+ privateInfo->endSegNo < entry->segno)
+ {
+ privateInfo->cur_wal = NULL;
+ continue;
+ }
+
+ /* WAL segments must be archived in order */
+ pg_log_error("WAL files are not archived in sequential order");
+ pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
+ segno, entry->segno);
+ exit(1);
+ }
+
+ /* Requested WAL segment not found */
+ XLogFileName(fname, privateInfo->timeline, segno, WalSegSz);
+ pg_fatal("could not find file \"%s\" in archive", fname);
+}
+
+/*
+ * Create an astreamer that can read WAL from tar file.
+ */
+static astreamer *
+astreamer_waldump_new(XLogDumpPrivate *privateInfo)
+{
+ astreamer_waldump *streamer;
+
+ streamer = palloc0(sizeof(astreamer_waldump));
+ *((const astreamer_ops **) &streamer->base.bbs_ops) =
+ &astreamer_waldump_ops;
+
+ streamer->privateInfo = privateInfo;
+
+ return &streamer->base;
+}
+
+/*
+ * Main entry point of the archive streamer for reading WAL data from a tar
+ * file. If a member is identified as a valid WAL file, a hash entry is created
+ * for it, and its contents are copied into that entry's buffer, making them
+ * accessible to the decoding routine.
+ */
+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:
+ {
+ XLogSegNo segno;
+ TimeLineID timeline;
+ ArchivedWALEntry *entry;
+ bool found;
+
+ pg_log_debug("pg_waldump: reading \"%s\"", member->pathname);
+
+ if (!member_is_wal_file(mystreamer, member,
+ &segno, &timeline))
+ break;
+
+ entry = ArchivedWAL_insert(ArchivedWAL_HTAB, segno, &found);
+
+ /*
+ * Shouldn't happen, but if it does, simply ignore the
+ * duplicate WAL file.
+ */
+ if (found)
+ {
+ pg_log_warning("ignoring duplicate WAL file found in archive: \"%s\"",
+ member->pathname);
+ break;
+ }
+
+ initStringInfo(&entry->buf);
+ entry->timeline = timeline;
+ entry->total_read = 0;
+
+ privateInfo->cur_wal = entry;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_CONTENTS:
+ if (privateInfo->cur_wal)
+ {
+ appendBinaryStringInfo(&privateInfo->cur_wal->buf, data, len);
+ privateInfo->cur_wal->total_read += len;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_TRAILER:
+ privateInfo->cur_wal = NULL;
+ 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)
+{
+ Assert(streamer->bbs_next == NULL);
+ pfree(streamer);
+}
+
+/*
+ * Returns true if the archive member name matches the WAL naming format. If
+ * successful, it also outputs the WAL segment number, and timeline.
+ */
+static bool
+member_is_wal_file(astreamer_waldump *mystreamer, astreamer_member *member,
+ XLogSegNo *curSegNo, TimeLineID *curTimeline)
+{
+ 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;
+
+ /*
+ * XXX: On some systems (e.g., OpenBSD), the tar utility includes
+ * PaxHeaders when creating an archive. These are special entries that
+ * store extended metadata for the file entry immediately following them,
+ * and they share the exact same name as that file.
+ */
+ if (strstr(member->pathname, "PaxHeaders."))
+ return false;
+
+ /* Parse position from file */
+ XLogFromFileName(fname, &timeline, &segNo, WalSegSz);
+
+ *curSegNo = segNo;
+ *curTimeline = timeline;
+
+ return true;
+}
diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build
index 937e0d68841..da00746587c 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',
+ 'archive_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 8d0cd9e7156..8a838f16ba2 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -177,7 +177,7 @@ split_path(const char *path, char **dir, char **fname)
*
* return a read only fd
*/
-static int
+int
open_file_in_directory(const char *directory, const char *fname)
{
int fd = -1;
@@ -436,6 +436,44 @@ 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);
+
+ if (private->endptr_reached)
+ return -1;
+
+ /* Read the WAL page from the archive streamer */
+ return read_archive_wal_page(private, targetPagePtr, count, readBuff);
+}
+
/*
* Boolean to return whether the given WAL record matches a specific relation
* and optionally block.
@@ -773,8 +811,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"
@@ -806,7 +844,10 @@ main(int argc, char **argv)
XLogRecord *record;
XLogRecPtr first_record;
char *waldir = NULL;
+ char *walpath = NULL;
char *errormsg;
+ bool is_archive = false;
+ pg_compress_algorithm compression;
static struct option long_options[] = {
{"bkp-details", no_argument, NULL, 'b'},
@@ -938,7 +979,7 @@ main(int argc, char **argv)
}
break;
case 'p':
- waldir = pg_strdup(optarg);
+ walpath = pg_strdup(optarg);
break;
case 'q':
config.quiet = true;
@@ -1102,10 +1143,27 @@ main(int argc, char **argv)
goto bad_argument;
}
- if (waldir != NULL)
+ if (walpath != NULL)
{
+ /* validate path points to tar archive */
+ if (is_archive_file(walpath, &compression))
+ {
+ char *fname = NULL;
+
+ split_path(walpath, &waldir, &fname);
+
+ /*
+ * A NULL WAL directory indicates that the archive file is located
+ * in the current working directory of the pg_waldump execution
+ */
+ if (waldir == NULL)
+ waldir = pg_strdup(".");
+
+ private.archive_name = fname;
+ is_archive = 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;
@@ -1123,46 +1181,36 @@ 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_archive)
+ {
+ 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_archive_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;
+ waldir = walpath ? pg_strdup(walpath) : pg_strdup(".");
+ private.archive_name = fname;
+ is_archive = 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)
@@ -1170,32 +1218,63 @@ 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_archive)
+ waldir = identify_target_directory(walpath, NULL);
/* we don't know what to print */
if (XLogRecPtrIsInvalid(private.startptr))
@@ -1207,12 +1286,30 @@ 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_archive)
+ {
+ /* Set up for reading tar file */
+ init_archive_reader(&private, waldir, compression);
+
+ /* 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");
@@ -1321,6 +1418,9 @@ main(int argc, char **argv)
XLogReaderFree(xlogreader_state);
+ if (is_archive)
+ free_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
index 9e62b64ead5..54758c3548a 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -12,9 +12,13 @@
#define PG_WALDUMP_H
#include "access/xlogdefs.h"
+#include "fe_utils/astreamer.h"
extern int WalSegSz;
+/* Forward declaration */
+struct ArchivedWALEntry;
+
/* Contains the necessary information to drive WAL decoding */
typedef struct XLogDumpPrivate
{
@@ -22,6 +26,36 @@ typedef struct XLogDumpPrivate
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;
+
+ /* What the archive streamer is currently reading */
+ struct ArchivedWALEntry *cur_wal;
+
+ /*
+ * Although these values can be easily derived from startptr and endptr,
+ * doing so repeatedly for each archived member would be inefficient, as
+ * it would involve recalculating and filtering out irrelevant WAL
+ * segments.
+ */
+ XLogSegNo startSegNo;
+ XLogSegNo endSegNo;
} XLogDumpPrivate;
-#endif /* end of PG_WALDUMP_H */
+extern int open_file_in_directory(const char *directory, const char *fname);
+
+extern bool is_archive_file(const char *fname,
+ pg_compress_algorithm *compression);
+extern void init_archive_reader(XLogDumpPrivate *privateInfo,
+ const char *waldir,
+ pg_compress_algorithm compression);
+extern void free_archive_reader(XLogDumpPrivate *privateInfo);
+extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
+ XLogRecPtr targetPagePtr,
+ Size count, char *readBuff);
+
+#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 1b712e8d74d..443126a9ce6 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -3,10 +3,13 @@
use strict;
use warnings FATAL => 'all';
+use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $tar = $ENV{TAR};
+
program_help_ok('pg_waldump');
program_version_ok('pg_waldump');
program_options_handling_ok('pg_waldump');
@@ -235,7 +238,7 @@ command_like(
sub test_pg_waldump
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my @opts = @_;
+ my ($path, @opts) = @_;
my ($stdout, $stderr);
@@ -243,6 +246,7 @@ sub test_pg_waldump
'pg_waldump',
'--start' => $start_lsn,
'--end' => $end_lsn,
+ '--path' => $path,
@opts
],
'>' => \$stdout,
@@ -254,11 +258,50 @@ sub test_pg_waldump
return @lines;
}
-my @lines;
+# 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 = sort @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
+ '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)
@@ -267,6 +310,19 @@ for my $scenario (@scenario)
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/,
@@ -298,38 +354,42 @@ for my $scenario (@scenario)
qr/error: error in WAL record at/,
'errors are shown with --quiet');
- @lines = test_pg_waldump('--path' => $path);
+ my @lines;
+ @lines = test_pg_waldump($path);
is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
- @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ @lines = test_pg_waldump($path, '--limit' => 6);
is(@lines, 6, 'limit option observed');
- @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ @lines = test_pg_waldump($path, '--fullpage');
is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
- @lines = test_pg_waldump('--path' => $path, '--stats');
+ @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' => $path, '--stats=record');
+ @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' => $path, '--rmgr' => 'Btree');
+ @lines = test_pg_waldump($path, '--rmgr' => 'Btree');
is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
- @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ @lines = test_pg_waldump($path, '--fork' => 'init');
is(grep(!/fork init/, @lines), 0, 'only init fork lines');
- @lines = test_pg_waldump('--path' => $path,
+ @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' => $path,
+ @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'};
}
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index bb4e1b37005..de2ad42bcab 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -139,6 +139,8 @@ ArchiveOpts
ArchiveShutdownCB
ArchiveStartupCB
ArchiveStreamState
+ArchivedWALEntry
+ArchivedWAL_hash
ArchiverOutput
ArchiverStage
ArrayAnalyzeExtraData
@@ -3453,6 +3455,7 @@ astreamer_recovery_injector
astreamer_tar_archiver
astreamer_tar_parser
astreamer_verify
+astreamer_waldump
astreamer_zstd_frame
auth_password_hook_typ
autovac_table
--
2.47.1
v5-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchapplication/x-patch; name=v5-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchDownload
From 866225e20f1389b94e25c40314b58332d7e0a6c5 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 6 Nov 2025 13:48:33 +0530
Subject: [PATCH v5 5/8] pg_waldump: Remove the restriction on the order of
archived WAL files.
With previous patch, pg_waldump would stop decoding if WAL files were
not in the required sequence. With this patch, decoding will now
continue. Any WAL file that is out of order will be written to a
temporary location, from which it will be read later. Once a temporary
file has been read, it will be removed.
---
src/bin/pg_waldump/archive_waldump.c | 207 +++++++++++++++++++++++++--
src/bin/pg_waldump/pg_waldump.c | 41 +++++-
src/bin/pg_waldump/pg_waldump.h | 4 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
4 files changed, 243 insertions(+), 12 deletions(-)
diff --git a/src/bin/pg_waldump/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
index e619e29d5d4..4a280b58ec2 100644
--- a/src/bin/pg_waldump/archive_waldump.c
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -17,6 +17,7 @@
#include <unistd.h>
#include "access/xlog_internal.h"
+#include "common/file_perm.h"
#include "common/hashfn.h"
#include "common/logging.h"
#include "fe_utils/simple_list.h"
@@ -27,6 +28,11 @@
*/
#define READ_CHUNK_SIZE (128 * 1024)
+#define TEMP_FILE_PREFIX "waldump.tmp"
+
+/* Temporary exported WAL file directory */
+static char *TmpWalSegDir = NULL;
+
/* Structure for storing the WAL segment data from the archive */
typedef struct ArchivedWALEntry
{
@@ -65,6 +71,11 @@ typedef struct astreamer_waldump
static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
XLogDumpPrivate *privateInfo);
+static void setup_tmpseg_dir(const char *waldir);
+static void cleanup_tmpseg_dir_atexit(void);
+
+static FILE *prepare_tmp_write(XLogSegNo segno);
+static void perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file);
static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
static void astreamer_waldump_content(astreamer *streamer,
@@ -120,10 +131,11 @@ is_archive_file(const char *fname, pg_compress_algorithm *compression)
}
/*
- * Initializes the tar archive reader to read WAL files from the archive,
- * creates a hash table to store them, performs quick existence checks for WAL
- * entries in the archive and retrieves the WAL segment size, and sets up
- * filtering criteria for relevant entries.
+ * Initializes the tar archive reader, creates a hash table for WAL entries,
+ * checks for existing valid WAL segments in the archive file and retrieves the
+ * segment size, and sets up filters for relevant entries. It also configures a
+ * temporary directory for out-of-order WAL data and registers an exit callback
+ * to clean up temporary files.
*/
void
init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
@@ -194,6 +206,13 @@ init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
*/
XLByteToSeg(privateInfo->startptr, privateInfo->startSegNo, WalSegSz);
XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+
+ /*
+ * Setup temporary directory to store WAL segments and set up an exit
+ * callback to remove it upon completion.
+ */
+ setup_tmpseg_dir(waldir);
+ atexit(cleanup_tmpseg_dir_atexit);
}
/*
@@ -362,13 +381,16 @@ read_archive_file(XLogDumpPrivate *privateInfo, Size count)
/*
* Returns the archived WAL entry from the hash table if it exists. Otherwise,
* it invokes the routine to read the archived file and retrieve the entry if
- * it is not already in hash table.
+ * it is not already present in the hash table. If the archive streamer happens
+ * to be reading a WAL from archive file that is not currently needed, that WAL
+ * data is written to a temporary file.
*/
static ArchivedWALEntry *
get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
{
ArchivedWALEntry *entry = NULL;
char fname[MAXFNAMELEN];
+ FILE *write_fp = NULL;
/* Search hash table */
entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
@@ -411,11 +433,32 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
continue;
}
- /* WAL segments must be archived in order */
- pg_log_error("WAL files are not archived in sequential order");
- pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
- segno, entry->segno);
- exit(1);
+ /*
+ * Archive streamer is currently reading a file that isn't the one
+ * asked for, but it's required for a future feature. It should be
+ * written to a temporary location for retrieval when needed.
+ */
+
+ /* Create a temporary file if one does not already exist */
+ if (!entry->tmpseg_exists)
+ {
+ write_fp = prepare_tmp_write(entry->segno);
+ entry->tmpseg_exists = true;
+ }
+
+ /* Flush data from the buffer to the file */
+ perform_tmp_write(entry->segno, &entry->buf, write_fp);
+ resetStringInfo(&entry->buf);
+
+ /*
+ * The change in the current segment entry indicates that the reading
+ * of this file has ended.
+ */
+ if (entry != privateInfo->cur_wal && write_fp != NULL)
+ {
+ fclose(write_fp);
+ write_fp = NULL;
+ }
}
/* Requested WAL segment not found */
@@ -423,6 +466,150 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
pg_fatal("could not find file \"%s\" in archive", fname);
}
+/*
+ * Set up a temporary directory to temporarily store WAL segments.
+ */
+static void
+setup_tmpseg_dir(const char *waldir)
+{
+ /*
+ * Use the directory specified by the TEMDIR environment variable. If it's
+ * not set, use the provided WAL directory to extract WAL file
+ * temporarily.
+ */
+ 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
+cleanup_tmpseg_dir_atexit(void)
+{
+ ArchivedWAL_iterator it;
+ ArchivedWALEntry *entry;
+
+ ArchivedWAL_start_iterate(ArchivedWAL_HTAB, &it);
+ while ((entry = ArchivedWAL_iterate(ArchivedWAL_HTAB, &it)) != NULL)
+ {
+ if (entry->tmpseg_exists)
+ {
+ remove_tmp_walseg(entry->segno, false);
+ entry->tmpseg_exists = false;
+ }
+ }
+}
+
+/*
+ * Generate the temporary WAL file path.
+ *
+ * Note that the caller is responsible to pfree it.
+ */
+char *
+get_tmp_walseg_path(XLogSegNo segno)
+{
+ char *fpath = (char *) palloc(MAXPGPATH);
+
+ snprintf(fpath, MAXPGPATH, "%s/%s.%08X%08X",
+ TmpWalSegDir,
+ TEMP_FILE_PREFIX,
+ (uint32) (segno / XLogSegmentsPerXLogId(WalSegSz)),
+ (uint32) (segno % XLogSegmentsPerXLogId(WalSegSz)));
+
+ return fpath;
+}
+
+/*
+ * Routine to check whether a temporary file exists for the corresponding WAL
+ * segment number.
+ */
+bool
+tmp_walseg_exists(XLogSegNo segno)
+{
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry == NULL)
+ return false;
+
+ return entry->tmpseg_exists;
+}
+
+/*
+ * Create an empty placeholder file and return its handle.
+ */
+static FILE *
+prepare_tmp_write(XLogSegNo segno)
+{
+ FILE *file;
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(segno);
+
+ /* 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_debug("temporarily exporting file \"%s\"", fpath);
+ pfree(fpath);
+
+ return file;
+}
+
+/*
+ * Write buffer data to the given file handle.
+ */
+static void
+perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file)
+{
+ Assert(file);
+
+ errno = 0;
+ if (buf->len > 0 && fwrite(buf->data, buf->len, 1, file) != 1)
+ {
+ /*
+ * 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_walseg_path(segno));
+ }
+}
+
+/*
+ * Remove temporary file
+ */
+void
+remove_tmp_walseg(XLogSegNo segno, bool update_entry)
+{
+ char *fpath = get_tmp_walseg_path(segno);
+
+ if (unlink(fpath) == 0)
+ pg_log_debug("removed file \"%s\"", fpath);
+ pfree(fpath);
+
+ /* Update entry if requested */
+ if (update_entry)
+ {
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+ Assert(entry != NULL);
+ entry->tmpseg_exists = false;
+ }
+}
+
/*
* Create an astreamer that can read WAL from tar file.
*/
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 8a838f16ba2..8acb7809645 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -466,11 +466,50 @@ TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
{
XLogDumpPrivate *private = state->private_data;
int count = required_read_len(private, targetPagePtr, reqLen);
+ XLogSegNo nextSegNo;
if (private->endptr_reached)
return -1;
- /* Read the WAL page from the archive streamer */
+ /*
+ * 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))
+ {
+ 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. */
+ remove_tmp_walseg(nextSegNo, true);
+ }
+
+ 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
+ */
+ if (tmp_walseg_exists(nextSegNo))
+ {
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(nextSegNo);
+ state->seg.ws_file = open(fpath, O_RDONLY | PG_BINARY, 0);
+ pfree(fpath);
+ }
+ }
+
+ /* Continue reading from the open WAL segment, if any */
+ if (state->seg.ws_file >= 0)
+ return WALDumpReadPage(state, targetPagePtr, count, targetPtr,
+ readBuff);
+
+ /* Otherwise, read the WAL page from the archive streamer */
return read_archive_wal_page(private, targetPagePtr, count, readBuff);
}
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
index 54758c3548a..5c1fb1e080a 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -58,4 +58,8 @@ extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
XLogRecPtr targetPagePtr,
Size count, char *readBuff);
+extern char *get_tmp_walseg_path(XLogSegNo segno);
+extern bool tmp_walseg_exists(XLogSegNo segno);
+extern void remove_tmp_walseg(XLogSegNo segno, bool update_entry);
+
#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 443126a9ce6..d5fa1f6d28d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -7,6 +7,7 @@ use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+use List::Util qw(shuffle);
my $tar = $ENV{TAR};
@@ -272,7 +273,7 @@ sub generate_archive
}
closedir $dh;
- @files = sort @files;
+ @files = shuffle @files;
# move into the WAL directory before archiving files
my $cwd = getcwd;
--
2.47.1
v5-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchapplication/x-patch; name=v5-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchDownload
From f750f5fece87a9f642225065a540ad4a2d209496 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 14:47:43 +0530
Subject: [PATCH v5 6/8] pg_verifybackup: Delay default WAL directory
preparation.
We are not sure whether to parse WAL from a directory or an archive
until the backup format is known. Therefore, we delay preparing the
default WAL directory until the point of parsing. This delay is
harmless, as the WAL directory is not used elsewhere.
---
src/bin/pg_verifybackup/pg_verifybackup.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 5e6c13bb921..31ebc1581fb 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -285,10 +285,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
@@ -368,6 +364,10 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
+ /* 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 parse the required ranges of WAL records, unless we were told
* not to do so.
--
2.47.1
v5-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchapplication/x-patch; name=v5-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchDownload
From 33299daf17137bded756aedbe122232cc4ecc244 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 24 Jul 2025 16:37:43 +0530
Subject: [PATCH v5 7/8] pg_verifybackup: Rename the wal-directory switch to
wal-path
With previous patches to pg_waldump can now decode WAL directly from
tar files. This means you'll be able to specify a tar archive path
instead of a traditional WAL directory.
To keep things consistent and more versatile, we should also
generalize the input switch for pg_verifybackup. It should accept
either a directory or a tar file path that contains WALs. This change
will also aligning it with the existing manifest-path switch naming.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 2 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 22 +++++++++++-----------
src/bin/pg_verifybackup/po/de.po | 4 ++--
src/bin/pg_verifybackup/po/el.po | 4 ++--
src/bin/pg_verifybackup/po/es.po | 4 ++--
src/bin/pg_verifybackup/po/fr.po | 4 ++--
src/bin/pg_verifybackup/po/it.po | 4 ++--
src/bin/pg_verifybackup/po/ja.po | 4 ++--
src/bin/pg_verifybackup/po/ka.po | 4 ++--
src/bin/pg_verifybackup/po/ko.po | 4 ++--
src/bin/pg_verifybackup/po/ru.po | 4 ++--
src/bin/pg_verifybackup/po/sv.po | 4 ++--
src/bin/pg_verifybackup/po/uk.po | 4 ++--
src/bin/pg_verifybackup/po/zh_CN.po | 4 ++--
src/bin/pg_verifybackup/po/zh_TW.po | 4 ++--
src/bin/pg_verifybackup/t/007_wal.pl | 4 ++--
16 files changed, 40 insertions(+), 40 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index 61c12975e4a..e9b8bfd51b1 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -261,7 +261,7 @@ PostgreSQL documentation
<varlistentry>
<term><option>-w <replaceable class="parameter">path</replaceable></option></term>
- <term><option>--wal-directory=<replaceable class="parameter">path</replaceable></option></term>
+ <term><option>--wal-path=<replaceable class="parameter">path</replaceable></option></term>
<listitem>
<para>
Try to parse WAL files stored in the specified directory, rather than
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 31ebc1581fb..1ee400199da 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -93,7 +93,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 +126,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 +135,7 @@ 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 *pg_waldump_path = NULL;
DIR *dir;
@@ -221,8 +221,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 */
@@ -365,15 +365,15 @@ main(int argc, char **argv)
verify_backup_checksums(&context);
/* By default, look for the WAL in the backup directory, too. */
- if (wal_directory == NULL)
- wal_directory = psprintf("%s/pg_wal", context.backup_directory);
+ if (wal_path == NULL)
+ wal_path = psprintf("%s/pg_wal", context.backup_directory);
/*
* 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
@@ -1198,7 +1198,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 +1208,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 +1376,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 a9e24931100..9b5cd5898cf 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 3e3f20c67c5..81442f51c17 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 0cb958f3448..7f729fa35ba 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 da8c72f6427..09937966fa7 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 317b0b71e7f..4da68d0074e 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 c910fb236cc..a948959b54f 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 982751984c7..ef2799316a8 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 acdc3da5e02..eaf91ef1e98 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 64005feedfd..7fb0e5ab1f6 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 17240feeb5c..97125838e8c 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 034b9764232..63f8041ab38 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 b7d97c8976d..fb6fcae8b82 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 c1b710b0a36..568f972b0bb 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/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl
index babc4f0a86b..b07f80719b0 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', '--no-parse-wal', $backup_path ],
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: $!";
--
2.47.1
v5-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchapplication/x-patch; name=v5-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchDownload
From 2498f315388fc8a1a840a2b883bca107b0113c0e Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 17 Jul 2025 16:39:36 +0530
Subject: [PATCH v5 8/8] pg_verifybackup: enabled WAL parsing for tar-format
backup
Now that pg_waldump supports decoding from tar archives, we should
leverage this functionality to remove the previous restriction on WAL
parsing for tar-backed formats.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 5 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 66 +++++++++++++------
src/bin/pg_verifybackup/t/002_algorithm.pl | 4 --
src/bin/pg_verifybackup/t/003_corruption.pl | 4 +-
src/bin/pg_verifybackup/t/008_untar.pl | 5 +-
src/bin/pg_verifybackup/t/010_client_untar.pl | 5 +-
6 files changed, 50 insertions(+), 39 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index e9b8bfd51b1..16b50b5a4df 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -36,10 +36,7 @@ PostgreSQL documentation
<literal>backup_manifest</literal> 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 <application>pg_basebackup</application>. However, at present,
- <literal>WAL</literal> verification is supported only for plain-format
- backups. Therefore, if the backup is stored in tar-format, the
- <literal>-n, --no-parse-wal</literal> option should be used.
+ supported by <application>pg_basebackup</application>.
</para>
<para>
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 1ee400199da..4bfe6fdff16 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);
@@ -136,6 +140,8 @@ main(int argc, char **argv)
bool no_parse_wal = false;
bool quiet = false;
char *wal_path = NULL;
+ char *base_archive_path = NULL;
+ char *wal_archive_path = NULL;
char *pg_waldump_path = NULL;
DIR *dir;
@@ -327,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'.
@@ -346,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.
@@ -364,9 +359,28 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
- /* By default, look for the WAL in the backup directory, too. */
+ /*
+ * 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)
- wal_path = psprintf("%s/pg_wal", context.backup_directory);
+ {
+ 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
@@ -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 <tablespaceoid>.tar file here. */
diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl
index ae16c11bc4d..4f284a9e828 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 1dd60f709cf..f1ebdbb46b4 100644
--- a/src/bin/pg_verifybackup/t/003_corruption.pl
+++ b/src/bin/pg_verifybackup/t/003_corruption.pl
@@ -193,10 +193,8 @@ for my $scenario (@scenario)
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/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl
index bc3d6b352ad..09079a94fee 100644
--- a/src/bin/pg_verifybackup/t/008_untar.pl
+++ b/src/bin/pg_verifybackup/t/008_untar.pl
@@ -47,7 +47,6 @@ my $tsoid = $primary->safe_psql(
SELECT oid FROM pg_tablespace WHERE spcname = 'regress_ts1'));
my $backup_path = $primary->backup_dir . '/server-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -123,14 +122,12 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
rmtree($backup_path);
- rmtree($extract_path);
}
}
diff --git a/src/bin/pg_verifybackup/t/010_client_untar.pl b/src/bin/pg_verifybackup/t/010_client_untar.pl
index b62faeb5acf..5b0e76ee69d 100644
--- a/src/bin/pg_verifybackup/t/010_client_untar.pl
+++ b/src/bin/pg_verifybackup/t/010_client_untar.pl
@@ -32,7 +32,6 @@ print $jf $junk_data;
close $jf;
my $backup_path = $primary->backup_dir . '/client-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -137,13 +136,11 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
- rmtree($extract_path);
rmtree($backup_path);
}
}
--
2.47.1
On Thu, Nov 6, 2025 at 2:33 PM Amul Sul <sulamul@gmail.com> wrote:
On Mon, Oct 20, 2025 at 8:05 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Oct 16, 2025 at 7:49 AM Amul Sul <sulamul@gmail.com> wrote:
[....]Kindly have a look at the attached version. Thank you !
Attached is the rebased version against the latest master head (e76defbcf09).
Regards,
Amul
Attachments:
v6-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchapplication/octet-stream; name=v6-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchDownload
From f56a3ce0343d9f539f638b88445aefb256afbfb5 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 24 Jun 2025 11:33:20 +0530
Subject: [PATCH v6 1/8] Refactor: pg_waldump: Move some declarations to new
pg_waldump.h
This change prepares for a second source file in this directory to
support reading WAL from tar files. Common structures, declarations,
and functions are being exported through this include file so
they can be used in both files.
---
src/bin/pg_waldump/pg_waldump.c | 11 ++---------
src/bin/pg_waldump/pg_waldump.h | 27 +++++++++++++++++++++++++++
2 files changed, 29 insertions(+), 9 deletions(-)
create mode 100644 src/bin/pg_waldump/pg_waldump.h
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index c6d6ba79e44..5846ee24f46 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,19 +40,11 @@
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};
-typedef struct XLogDumpPrivate
-{
- TimeLineID timeline;
- XLogRecPtr startptr;
- XLogRecPtr endptr;
- bool endptr_reached;
-} XLogDumpPrivate;
-
typedef struct XLogDumpConfig
{
/* display options */
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
new file mode 100644
index 00000000000..9e62b64ead5
--- /dev/null
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -0,0 +1,27 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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"
+
+extern int WalSegSz;
+
+/* Contains the necessary information to drive WAL decoding */
+typedef struct XLogDumpPrivate
+{
+ TimeLineID timeline;
+ XLogRecPtr startptr;
+ XLogRecPtr endptr;
+ bool endptr_reached;
+} XLogDumpPrivate;
+
+#endif /* end of PG_WALDUMP_H */
--
2.47.1
v6-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchapplication/octet-stream; name=v6-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchDownload
From 835cce8a5ed331215cf6c77075ed0ddea06ee859 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 26 Jun 2025 11:42:53 +0530
Subject: [PATCH v6 2/8] Refactor: pg_waldump: Separate logic used to calculate
the required read size.
This refactoring prepares the codebase for an upcoming patch that will
support reading WAL from tar files. The logic for calculating the
required read size has been updated to handle both normal WAL files
and WAL files located inside a tar archive.
---
src/bin/pg_waldump/pg_waldump.c | 39 ++++++++++++++++++++++-----------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 5846ee24f46..0dc28ea360c 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -326,6 +326,29 @@ identify_target_directory(char *directory, char *fname)
return NULL; /* not reached */
}
+/* 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 (XLogRecPtrIsValid(private->endptr))
+ {
+ 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,
@@ -383,21 +406,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 (XLogRecPtrIsValid(private->endptr))
- {
- 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))
--
2.47.1
v6-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchapplication/octet-stream; name=v6-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchDownload
From b4d347153a0b1eed353a0549331a52c232aa04ac Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 30 Jul 2025 12:43:30 +0530
Subject: [PATCH v6 3/8] Refactor: pg_waldump: Restructure TAP tests.
Restructured some tests to run inside a loop, facilitating their
re-execution for decoding WAL from tar archives.
---
src/bin/pg_waldump/t/001_basic.pl | 123 ++++++++++++++++--------------
1 file changed, 67 insertions(+), 56 deletions(-)
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index f26d75e01cf..1b712e8d74d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -198,28 +198,6 @@ command_like(
],
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 +205,6 @@ command_like(
],
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.
@@ -272,7 +241,6 @@ sub test_pg_waldump
my $result = IPC::Run::run [
'pg_waldump',
- '--path' => $node->data_dir,
'--start' => $start_lsn,
'--end' => $end_lsn,
@opts
@@ -288,38 +256,81 @@ sub test_pg_waldump
my @lines;
-@lines = test_pg_waldump;
-is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
+my @scenario = (
+ {
+ 'path' => $node->data_dir
+ });
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenario)
+{
+ my $path = $scenario->{'path'};
-@lines = test_pg_waldump('--fullpage');
-is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
+ SKIP:
+ {
+ 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');
-@lines = test_pg_waldump('--stats');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+ command_fails_like(
+ [
+ 'pg_waldump', '--quiet',
+ '--path' => $path,
+ '--start' => $start_lsn
+ ],
+ qr/error: error in WAL record at/,
+ 'errors are shown with --quiet');
-@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('--path' => $path);
+ is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
-@lines = test_pg_waldump('--rmgr' => 'Btree');
-is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+ @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ is(@lines, 6, 'limit option observed');
-@lines = test_pg_waldump('--fork' => 'init');
-is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+ @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
-@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('--path' => $path, '--stats');
+ like($lines[0], qr/WAL statistics/, "statistics on stdout");
+ is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
-@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');
+ @lines = test_pg_waldump('--path' => $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' => $path, '--rmgr' => 'Btree');
+ is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+
+ @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+
+ @lines = test_pg_waldump('--path' => $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' => $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');
+ }
+}
done_testing();
--
2.47.1
v6-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchapplication/octet-stream; name=v6-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchDownload
From 187e47acc12b4983a13c9c4aad7fbc66f92db0f6 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 5 Nov 2025 15:40:36 +0530
Subject: [PATCH v6 4/8] pg_waldump: Add support for archived WAL decoding.
pg_waldump can now accept the path to a tar archive containing WAL
files and decode them. This feature was added primarily for
pg_verifybackup, which previously disabled WAL parsing for
tar-formatted backups.
Note that this patch requires that the WAL files within the archive be
in sequential order; an error will be reported otherwise. The next
patch is planned to remove this restriction.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/Makefile | 7 +-
src/bin/pg_waldump/archive_waldump.c | 577 +++++++++++++++++++++++++++
src/bin/pg_waldump/meson.build | 4 +-
src/bin/pg_waldump/pg_waldump.c | 217 +++++++---
src/bin/pg_waldump/pg_waldump.h | 36 +-
src/bin/pg_waldump/t/001_basic.pl | 84 +++-
src/tools/pgindent/typedefs.list | 3 +
8 files changed, 860 insertions(+), 76 deletions(-)
create mode 100644 src/bin/pg_waldump/archive_waldump.c
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index ce23add5577..d004bb0f67e 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -141,13 +141,17 @@ PostgreSQL documentation
<term><option>--path=<replaceable>path</replaceable></option></term>
<listitem>
<para>
- Specifies a directory to search for WAL segment files or a
- directory with a <literal>pg_wal</literal> subdirectory that
+ Specifies a tar archive or a directory to search for WAL segment files
+ or a directory with a <literal>pg_wal</literal> subdirectory that
contains such files. The default is to search in the current
directory, the <literal>pg_wal</literal> subdirectory of the
current directory, and the <literal>pg_wal</literal> subdirectory
of <envar>PGDATA</envar>.
</para>
+ <para>
+ If a tar archive is provided, its WAL segment files must be in
+ sequential order; otherwise, an error will be reported.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile
index 4c1ee649501..05ac5763a57 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 \
+ archive_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/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
new file mode 100644
index 00000000000..2830c89a7be
--- /dev/null
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -0,0 +1,577 @@
+/*-------------------------------------------------------------------------
+ *
+ * archive_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/archive_waldump.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <unistd.h>
+
+#include "access/xlog_internal.h"
+#include "common/hashfn.h"
+#include "common/logging.h"
+#include "fe_utils/simple_list.h"
+#include "pg_waldump.h"
+
+/*
+ * How many bytes should we try to read from a file at once?
+ */
+#define READ_CHUNK_SIZE (128 * 1024)
+
+/* Structure for storing the WAL segment data from the archive */
+typedef struct ArchivedWALEntry
+{
+ uint32 status; /* hash status */
+ XLogSegNo segno; /* hash key: WAL segment number */
+ TimeLineID timeline; /* timeline of this wal file */
+
+ StringInfoData buf;
+ bool tmpseg_exists; /* spill file exists? */
+
+ int total_read; /* total read of this WAL segment, including
+ * buffered and temporarily written data */
+} ArchivedWALEntry;
+
+#define SH_PREFIX ArchivedWAL
+#define SH_ELEMENT_TYPE ArchivedWALEntry
+#define SH_KEY_TYPE XLogSegNo
+#define SH_KEY segno
+#define SH_HASH_KEY(tb, key) murmurhash64((uint64) key)
+#define SH_EQUAL(tb, a, b) (a == b)
+#define SH_GET_HASH(tb, a) a->hash
+#define SH_SCOPE static inline
+#define SH_RAW_ALLOCATOR pg_malloc0
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
+
+static ArchivedWAL_hash *ArchivedWAL_HTAB = NULL;
+
+typedef struct astreamer_waldump
+{
+ astreamer base;
+ XLogDumpPrivate *privateInfo;
+} astreamer_waldump;
+
+static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
+static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
+ XLogDumpPrivate *privateInfo);
+
+static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
+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_wal_file(astreamer_waldump *mystreamer,
+ astreamer_member *member,
+ XLogSegNo *curSegNo,
+ TimeLineID *curTimeline);
+
+static const astreamer_ops astreamer_waldump_ops = {
+ .content = astreamer_waldump_content,
+ .finalize = astreamer_waldump_finalize,
+ .free = astreamer_waldump_free
+};
+
+/*
+ * Returns true if the given file is a tar archive and outputs its compression
+ * algorithm.
+ */
+bool
+is_archive_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;
+}
+
+/*
+ * Initializes the tar archive reader to read WAL files from the archive,
+ * creates a hash table to store them, performs quick existence checks for WAL
+ * entries in the archive and retrieves the WAL segment size, and sets up
+ * filtering criteria for relevant entries.
+ */
+void
+init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
+ pg_compress_algorithm compression)
+{
+ int fd;
+ astreamer *streamer;
+ ArchivedWALEntry *entry = NULL;
+ XLogLongPageHeader longhdr;
+
+ /* Open tar archive and store its file descriptor */
+ fd = open_file_in_directory(waldir, privateInfo->archive_name);
+
+ if (fd < 0)
+ pg_fatal("could not open file \"%s\"", privateInfo->archive_name);
+
+ privateInfo->archive_fd = fd;
+
+ streamer = astreamer_waldump_new(privateInfo);
+
+ /* 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);
+
+ privateInfo->archive_streamer = streamer;
+
+ /* Hash table storing WAL entries read from the archive */
+ ArchivedWAL_HTAB = ArchivedWAL_create(16, NULL);
+
+ /*
+ * Verify that the archive contains valid WAL files and fetch WAL segment
+ * size
+ */
+ while (entry == NULL || entry->buf.len < XLOG_BLCKSZ)
+ {
+ if (read_archive_file(privateInfo, XLOG_BLCKSZ) == 0)
+ pg_fatal("could not find WAL in \"%s\" archive",
+ privateInfo->archive_name);
+
+ entry = privateInfo->cur_wal;
+ }
+
+ /* Set WalSegSz if WAL data is successfully read */
+ longhdr = (XLogLongPageHeader) entry->buf.data;
+
+ WalSegSz = longhdr->xlp_seg_size;
+
+ if (!IsValidWalSegSize(WalSegSz))
+ {
+ pg_log_error(ngettext("invalid WAL segment size in WAL file from archive \"%s\" (%d byte)",
+ "invalid WAL segment size in WAL file from archive \"%s\" (%d bytes)",
+ WalSegSz),
+ privateInfo->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);
+ }
+
+ /*
+ * With the WAL segment size available, we can now initialize the
+ * dependent start and end segment numbers.
+ */
+ XLByteToSeg(privateInfo->startptr, privateInfo->startSegNo, WalSegSz);
+ XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+}
+
+/*
+ * Release the archive streamer chain and close the archive file.
+ */
+void
+free_archive_reader(XLogDumpPrivate *privateInfo)
+{
+ /*
+ * 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(privateInfo->archive_streamer);
+
+ /* Close the file. */
+ if (close(privateInfo->archive_fd) != 0)
+ pg_log_error("could not close file \"%s\": %m",
+ privateInfo->archive_name);
+}
+
+/*
+ * Copies WAL data from astreamer to readBuff; if unavailable, fetches more
+ * from the tar archive via astreamer.
+ */
+int
+read_archive_wal_page(XLogDumpPrivate *privateInfo, XLogRecPtr targetPagePtr,
+ Size count, char *readBuff)
+{
+ char *p = readBuff;
+ Size nbytes = count;
+ XLogRecPtr recptr = targetPagePtr;
+ XLogSegNo segno;
+ ArchivedWALEntry *entry;
+
+ XLByteToSeg(targetPagePtr, segno, WalSegSz);
+ entry = get_archive_wal_entry(segno, privateInfo);
+
+ while (nbytes > 0)
+ {
+ char *buf = entry->buf.data;
+ int len = entry->buf.len;
+
+ /* WAL record range that the buffer contains */
+ XLogRecPtr endPtr;
+ XLogRecPtr startPtr;
+
+ XLogSegNoOffsetToRecPtr(entry->segno, entry->total_read,
+ WalSegSz, endPtr);
+ startPtr = endPtr - len;
+
+ Assert((endPtr - startPtr) == len);
+
+ /*
+ * 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(&entry->buf);
+ }
+
+ if (len > 0 && recptr > startPtr)
+ {
+ int skipBytes = 0;
+
+ /*
+ * The required offset is not at the start of the 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 reading correct WAL record */
+ Assert(recptr >= startPtr && recptr < endPtr);
+
+ memcpy(p, buf, readBytes);
+
+ /* Update state for read */
+ nbytes -= readBytes;
+ p += readBytes;
+ recptr += readBytes;
+ }
+ else
+ {
+ /*
+ * Fetch more data; raise an error if it's not the current segment
+ * being read by the archive streamer or if reading of the
+ * archived file has finished.
+ */
+ if (privateInfo->cur_wal != entry ||
+ read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ {
+ char fname[MAXFNAMELEN];
+
+ XLogFileName(fname, privateInfo->timeline, entry->segno,
+ WalSegSz);
+ pg_fatal("could not read file \"%s\" from archive \"%s\": read %lld of %lld",
+ fname, privateInfo->archive_name,
+ (long long int) count - nbytes,
+ (long long int) nbytes);
+ }
+ }
+ }
+
+ /*
+ * Should have either have successfully read all the requested bytes or
+ * reported a failure before this point.
+ */
+ Assert(nbytes == 0);
+
+ /*
+ * NB: We return the fixed value provided as input. Although we could
+ * return a boolean since we either successfully read the WAL page or
+ * raise an error, but the caller expects this value to be returned. The
+ * routine that reads WAL pages from the physical WAL file follows the
+ * same convention.
+ */
+ return count;
+}
+
+/*
+ * Reads the archive file and passes it to the archive streamer for
+ * decompression.
+ */
+static int
+read_archive_file(XLogDumpPrivate *privateInfo, Size count)
+{
+ int rc;
+ char *buffer;
+
+ buffer = pg_malloc(READ_CHUNK_SIZE * sizeof(uint8));
+
+ rc = read(privateInfo->archive_fd, buffer, count);
+ if (rc < 0)
+ pg_fatal("could not read file \"%s\": %m",
+ privateInfo->archive_name);
+
+ /*
+ * Decompress (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;
+}
+
+/*
+ * Returns the archived WAL entry from the hash table if it exists. Otherwise,
+ * it invokes the routine to read the archived file and retrieve the entry if
+ * it is not already in hash table.
+ */
+static ArchivedWALEntry *
+get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
+{
+ ArchivedWALEntry *entry = NULL;
+ char fname[MAXFNAMELEN];
+
+ /* Search hash table */
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry != NULL)
+ return entry;
+
+ /* Needed WAL yet to be decoded from archive, do the same */
+ while (1)
+ {
+ entry = privateInfo->cur_wal;
+
+ /* Fetch more data */
+ if (entry == NULL || entry->buf.len == 0)
+ {
+ if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ break; /* archive file ended */
+ }
+
+ /*
+ * Either, here for the first time, or the archived streamer is
+ * reading a non-WAL file or an irrelevant WAL file.
+ */
+ if (entry == NULL)
+ continue;
+
+ /* Found the required entry */
+ if (entry->segno == segno)
+ return entry;
+
+ /*
+ * Ignore if the timeline is different or the current segment is not
+ * the desired one.
+ */
+ if (privateInfo->timeline != entry->timeline ||
+ privateInfo->startSegNo > entry->segno ||
+ privateInfo->endSegNo < entry->segno)
+ {
+ privateInfo->cur_wal = NULL;
+ continue;
+ }
+
+ /* WAL segments must be archived in order */
+ pg_log_error("WAL files are not archived in sequential order");
+ pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
+ segno, entry->segno);
+ exit(1);
+ }
+
+ /* Requested WAL segment not found */
+ XLogFileName(fname, privateInfo->timeline, segno, WalSegSz);
+ pg_fatal("could not find file \"%s\" in archive", fname);
+}
+
+/*
+ * Create an astreamer that can read WAL from tar file.
+ */
+static astreamer *
+astreamer_waldump_new(XLogDumpPrivate *privateInfo)
+{
+ astreamer_waldump *streamer;
+
+ streamer = palloc0(sizeof(astreamer_waldump));
+ *((const astreamer_ops **) &streamer->base.bbs_ops) =
+ &astreamer_waldump_ops;
+
+ streamer->privateInfo = privateInfo;
+
+ return &streamer->base;
+}
+
+/*
+ * Main entry point of the archive streamer for reading WAL data from a tar
+ * file. If a member is identified as a valid WAL file, a hash entry is created
+ * for it, and its contents are copied into that entry's buffer, making them
+ * accessible to the decoding routine.
+ */
+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:
+ {
+ XLogSegNo segno;
+ TimeLineID timeline;
+ ArchivedWALEntry *entry;
+ bool found;
+
+ pg_log_debug("reading \"%s\"", member->pathname);
+
+ if (!member_is_wal_file(mystreamer, member,
+ &segno, &timeline))
+ break;
+
+ entry = ArchivedWAL_insert(ArchivedWAL_HTAB, segno, &found);
+
+ /*
+ * Shouldn't happen, but if it does, simply ignore the
+ * duplicate WAL file.
+ */
+ if (found)
+ {
+ pg_log_warning("ignoring duplicate WAL file found in archive: \"%s\"",
+ member->pathname);
+ break;
+ }
+
+ initStringInfo(&entry->buf);
+ entry->timeline = timeline;
+ entry->total_read = 0;
+
+ privateInfo->cur_wal = entry;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_CONTENTS:
+ if (privateInfo->cur_wal)
+ {
+ appendBinaryStringInfo(&privateInfo->cur_wal->buf, data, len);
+ privateInfo->cur_wal->total_read += len;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_TRAILER:
+ privateInfo->cur_wal = NULL;
+ 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)
+{
+ Assert(streamer->bbs_next == NULL);
+ pfree(streamer);
+}
+
+/*
+ * Returns true if the archive member name matches the WAL naming format. If
+ * successful, it also outputs the WAL segment number, and timeline.
+ */
+static bool
+member_is_wal_file(astreamer_waldump *mystreamer, astreamer_member *member,
+ XLogSegNo *curSegNo, TimeLineID *curTimeline)
+{
+ 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;
+
+ /*
+ * XXX: On some systems (e.g., OpenBSD), the tar utility includes
+ * PaxHeaders when creating an archive. These are special entries that
+ * store extended metadata for the file entry immediately following them,
+ * and they share the exact same name as that file.
+ */
+ if (strstr(member->pathname, "PaxHeaders."))
+ return false;
+
+ /* Parse position from file */
+ XLogFromFileName(fname, &timeline, &segNo, WalSegSz);
+
+ *curSegNo = segNo;
+ *curTimeline = timeline;
+
+ return true;
+}
diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build
index 937e0d68841..da00746587c 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',
+ 'archive_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 0dc28ea360c..7425d386d0c 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -177,7 +177,7 @@ split_path(const char *path, char **dir, char **fname)
*
* return a read only fd
*/
-static int
+int
open_file_in_directory(const char *directory, const char *fname)
{
int fd = -1;
@@ -436,6 +436,44 @@ 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);
+
+ if (private->endptr_reached)
+ return -1;
+
+ /* Read the WAL page from the archive streamer */
+ return read_archive_wal_page(private, targetPagePtr, count, readBuff);
+}
+
/*
* Boolean to return whether the given WAL record matches a specific relation
* and optionally block.
@@ -773,8 +811,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"
@@ -806,7 +844,10 @@ main(int argc, char **argv)
XLogRecord *record;
XLogRecPtr first_record;
char *waldir = NULL;
+ char *walpath = NULL;
char *errormsg;
+ bool is_archive = false;
+ pg_compress_algorithm compression;
static struct option long_options[] = {
{"bkp-details", no_argument, NULL, 'b'},
@@ -938,7 +979,7 @@ main(int argc, char **argv)
}
break;
case 'p':
- waldir = pg_strdup(optarg);
+ walpath = pg_strdup(optarg);
break;
case 'q':
config.quiet = true;
@@ -1102,10 +1143,27 @@ main(int argc, char **argv)
goto bad_argument;
}
- if (waldir != NULL)
+ if (walpath != NULL)
{
+ /* validate path points to tar archive */
+ if (is_archive_file(walpath, &compression))
+ {
+ char *fname = NULL;
+
+ split_path(walpath, &waldir, &fname);
+
+ /*
+ * A NULL WAL directory indicates that the archive file is located
+ * in the current working directory of the pg_waldump execution
+ */
+ if (waldir == NULL)
+ waldir = pg_strdup(".");
+
+ private.archive_name = fname;
+ is_archive = 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;
@@ -1123,6 +1181,17 @@ 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_archive)
+ {
+ 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)
@@ -1133,69 +1202,78 @@ main(int argc, char **argv)
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 (!XLogRecPtrIsValid(private.startptr))
- XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr);
- else if (!XLByteInSeg(private.startptr, segno, WalSegSz))
+ if (fname != NULL && is_archive_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;
+ waldir = walpath ? pg_strdup(walpath) : pg_strdup(".");
+ private.archive_name = fname;
+ is_archive = true;
}
-
- /* no second file specified, set end position */
- if (!(optind + 1 < argc) && !XLogRecPtrIsValid(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(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, &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 (!XLogRecPtrIsValid(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 (!XLogRecPtrIsValid(private.endptr))
- XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz,
- private.endptr);
+ /* no second file specified, set end position */
+ if (!(optind + 1 < argc) && !XLogRecPtrIsValid(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 (!XLogRecPtrIsValid(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_archive)
+ waldir = identify_target_directory(walpath, NULL);
/* we don't know what to print */
if (!XLogRecPtrIsValid(private.startptr))
@@ -1207,12 +1285,30 @@ 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_archive)
+ {
+ /* Set up for reading tar file */
+ init_archive_reader(&private, waldir, compression);
+
+ /* 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");
@@ -1321,6 +1417,9 @@ main(int argc, char **argv)
XLogReaderFree(xlogreader_state);
+ if (is_archive)
+ free_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
index 9e62b64ead5..54758c3548a 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -12,9 +12,13 @@
#define PG_WALDUMP_H
#include "access/xlogdefs.h"
+#include "fe_utils/astreamer.h"
extern int WalSegSz;
+/* Forward declaration */
+struct ArchivedWALEntry;
+
/* Contains the necessary information to drive WAL decoding */
typedef struct XLogDumpPrivate
{
@@ -22,6 +26,36 @@ typedef struct XLogDumpPrivate
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;
+
+ /* What the archive streamer is currently reading */
+ struct ArchivedWALEntry *cur_wal;
+
+ /*
+ * Although these values can be easily derived from startptr and endptr,
+ * doing so repeatedly for each archived member would be inefficient, as
+ * it would involve recalculating and filtering out irrelevant WAL
+ * segments.
+ */
+ XLogSegNo startSegNo;
+ XLogSegNo endSegNo;
} XLogDumpPrivate;
-#endif /* end of PG_WALDUMP_H */
+extern int open_file_in_directory(const char *directory, const char *fname);
+
+extern bool is_archive_file(const char *fname,
+ pg_compress_algorithm *compression);
+extern void init_archive_reader(XLogDumpPrivate *privateInfo,
+ const char *waldir,
+ pg_compress_algorithm compression);
+extern void free_archive_reader(XLogDumpPrivate *privateInfo);
+extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
+ XLogRecPtr targetPagePtr,
+ Size count, char *readBuff);
+
+#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 1b712e8d74d..443126a9ce6 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -3,10 +3,13 @@
use strict;
use warnings FATAL => 'all';
+use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $tar = $ENV{TAR};
+
program_help_ok('pg_waldump');
program_version_ok('pg_waldump');
program_options_handling_ok('pg_waldump');
@@ -235,7 +238,7 @@ command_like(
sub test_pg_waldump
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my @opts = @_;
+ my ($path, @opts) = @_;
my ($stdout, $stderr);
@@ -243,6 +246,7 @@ sub test_pg_waldump
'pg_waldump',
'--start' => $start_lsn,
'--end' => $end_lsn,
+ '--path' => $path,
@opts
],
'>' => \$stdout,
@@ -254,11 +258,50 @@ sub test_pg_waldump
return @lines;
}
-my @lines;
+# 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 = sort @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
+ '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)
@@ -267,6 +310,19 @@ for my $scenario (@scenario)
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/,
@@ -298,38 +354,42 @@ for my $scenario (@scenario)
qr/error: error in WAL record at/,
'errors are shown with --quiet');
- @lines = test_pg_waldump('--path' => $path);
+ my @lines;
+ @lines = test_pg_waldump($path);
is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
- @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ @lines = test_pg_waldump($path, '--limit' => 6);
is(@lines, 6, 'limit option observed');
- @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ @lines = test_pg_waldump($path, '--fullpage');
is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
- @lines = test_pg_waldump('--path' => $path, '--stats');
+ @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' => $path, '--stats=record');
+ @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' => $path, '--rmgr' => 'Btree');
+ @lines = test_pg_waldump($path, '--rmgr' => 'Btree');
is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
- @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ @lines = test_pg_waldump($path, '--fork' => 'init');
is(grep(!/fork init/, @lines), 0, 'only init fork lines');
- @lines = test_pg_waldump('--path' => $path,
+ @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' => $path,
+ @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'};
}
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 23bce72ae64..0c8d6bfa3e1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -139,6 +139,8 @@ ArchiveOpts
ArchiveShutdownCB
ArchiveStartupCB
ArchiveStreamState
+ArchivedWALEntry
+ArchivedWAL_hash
ArchiverOutput
ArchiverStage
ArrayAnalyzeExtraData
@@ -3461,6 +3463,7 @@ astreamer_recovery_injector
astreamer_tar_archiver
astreamer_tar_parser
astreamer_verify
+astreamer_waldump
astreamer_zstd_frame
auth_password_hook_typ
autovac_table
--
2.47.1
v6-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchapplication/octet-stream; name=v6-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchDownload
From 7422ff4700b7493eef3c9097e86f99da5aaeeac3 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 6 Nov 2025 13:48:33 +0530
Subject: [PATCH v6 5/8] pg_waldump: Remove the restriction on the order of
archived WAL files.
With previous patch, pg_waldump would stop decoding if WAL files were
not in the required sequence. With this patch, decoding will now
continue. Any WAL file that is out of order will be written to a
temporary location, from which it will be read later. Once a temporary
file has been read, it will be removed.
---
src/bin/pg_waldump/archive_waldump.c | 207 +++++++++++++++++++++++++--
src/bin/pg_waldump/pg_waldump.c | 41 +++++-
src/bin/pg_waldump/pg_waldump.h | 4 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
4 files changed, 243 insertions(+), 12 deletions(-)
diff --git a/src/bin/pg_waldump/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
index 2830c89a7be..7c8f17ba135 100644
--- a/src/bin/pg_waldump/archive_waldump.c
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -17,6 +17,7 @@
#include <unistd.h>
#include "access/xlog_internal.h"
+#include "common/file_perm.h"
#include "common/hashfn.h"
#include "common/logging.h"
#include "fe_utils/simple_list.h"
@@ -27,6 +28,11 @@
*/
#define READ_CHUNK_SIZE (128 * 1024)
+#define TEMP_FILE_PREFIX "waldump.tmp"
+
+/* Temporary exported WAL file directory */
+static char *TmpWalSegDir = NULL;
+
/* Structure for storing the WAL segment data from the archive */
typedef struct ArchivedWALEntry
{
@@ -65,6 +71,11 @@ typedef struct astreamer_waldump
static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
XLogDumpPrivate *privateInfo);
+static void setup_tmpseg_dir(const char *waldir);
+static void cleanup_tmpseg_dir_atexit(void);
+
+static FILE *prepare_tmp_write(XLogSegNo segno);
+static void perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file);
static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
static void astreamer_waldump_content(astreamer *streamer,
@@ -120,10 +131,11 @@ is_archive_file(const char *fname, pg_compress_algorithm *compression)
}
/*
- * Initializes the tar archive reader to read WAL files from the archive,
- * creates a hash table to store them, performs quick existence checks for WAL
- * entries in the archive and retrieves the WAL segment size, and sets up
- * filtering criteria for relevant entries.
+ * Initializes the tar archive reader, creates a hash table for WAL entries,
+ * checks for existing valid WAL segments in the archive file and retrieves the
+ * segment size, and sets up filters for relevant entries. It also configures a
+ * temporary directory for out-of-order WAL data and registers an exit callback
+ * to clean up temporary files.
*/
void
init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
@@ -194,6 +206,13 @@ init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
*/
XLByteToSeg(privateInfo->startptr, privateInfo->startSegNo, WalSegSz);
XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+
+ /*
+ * Setup temporary directory to store WAL segments and set up an exit
+ * callback to remove it upon completion.
+ */
+ setup_tmpseg_dir(waldir);
+ atexit(cleanup_tmpseg_dir_atexit);
}
/*
@@ -362,13 +381,16 @@ read_archive_file(XLogDumpPrivate *privateInfo, Size count)
/*
* Returns the archived WAL entry from the hash table if it exists. Otherwise,
* it invokes the routine to read the archived file and retrieve the entry if
- * it is not already in hash table.
+ * it is not already present in the hash table. If the archive streamer happens
+ * to be reading a WAL from archive file that is not currently needed, that WAL
+ * data is written to a temporary file.
*/
static ArchivedWALEntry *
get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
{
ArchivedWALEntry *entry = NULL;
char fname[MAXFNAMELEN];
+ FILE *write_fp = NULL;
/* Search hash table */
entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
@@ -411,11 +433,32 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
continue;
}
- /* WAL segments must be archived in order */
- pg_log_error("WAL files are not archived in sequential order");
- pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
- segno, entry->segno);
- exit(1);
+ /*
+ * Archive streamer is currently reading a file that isn't the one
+ * asked for, but it's required for a future feature. It should be
+ * written to a temporary location for retrieval when needed.
+ */
+
+ /* Create a temporary file if one does not already exist */
+ if (!entry->tmpseg_exists)
+ {
+ write_fp = prepare_tmp_write(entry->segno);
+ entry->tmpseg_exists = true;
+ }
+
+ /* Flush data from the buffer to the file */
+ perform_tmp_write(entry->segno, &entry->buf, write_fp);
+ resetStringInfo(&entry->buf);
+
+ /*
+ * The change in the current segment entry indicates that the reading
+ * of this file has ended.
+ */
+ if (entry != privateInfo->cur_wal && write_fp != NULL)
+ {
+ fclose(write_fp);
+ write_fp = NULL;
+ }
}
/* Requested WAL segment not found */
@@ -423,6 +466,150 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
pg_fatal("could not find file \"%s\" in archive", fname);
}
+/*
+ * Set up a temporary directory to temporarily store WAL segments.
+ */
+static void
+setup_tmpseg_dir(const char *waldir)
+{
+ /*
+ * Use the directory specified by the TEMDIR environment variable. If it's
+ * not set, use the provided WAL directory to extract WAL file
+ * temporarily.
+ */
+ 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
+cleanup_tmpseg_dir_atexit(void)
+{
+ ArchivedWAL_iterator it;
+ ArchivedWALEntry *entry;
+
+ ArchivedWAL_start_iterate(ArchivedWAL_HTAB, &it);
+ while ((entry = ArchivedWAL_iterate(ArchivedWAL_HTAB, &it)) != NULL)
+ {
+ if (entry->tmpseg_exists)
+ {
+ remove_tmp_walseg(entry->segno, false);
+ entry->tmpseg_exists = false;
+ }
+ }
+}
+
+/*
+ * Generate the temporary WAL file path.
+ *
+ * Note that the caller is responsible to pfree it.
+ */
+char *
+get_tmp_walseg_path(XLogSegNo segno)
+{
+ char *fpath = (char *) palloc(MAXPGPATH);
+
+ snprintf(fpath, MAXPGPATH, "%s/%s.%08X%08X",
+ TmpWalSegDir,
+ TEMP_FILE_PREFIX,
+ (uint32) (segno / XLogSegmentsPerXLogId(WalSegSz)),
+ (uint32) (segno % XLogSegmentsPerXLogId(WalSegSz)));
+
+ return fpath;
+}
+
+/*
+ * Routine to check whether a temporary file exists for the corresponding WAL
+ * segment number.
+ */
+bool
+tmp_walseg_exists(XLogSegNo segno)
+{
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry == NULL)
+ return false;
+
+ return entry->tmpseg_exists;
+}
+
+/*
+ * Create an empty placeholder file and return its handle.
+ */
+static FILE *
+prepare_tmp_write(XLogSegNo segno)
+{
+ FILE *file;
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(segno);
+
+ /* 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_debug("temporarily exporting file \"%s\"", fpath);
+ pfree(fpath);
+
+ return file;
+}
+
+/*
+ * Write buffer data to the given file handle.
+ */
+static void
+perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file)
+{
+ Assert(file);
+
+ errno = 0;
+ if (buf->len > 0 && fwrite(buf->data, buf->len, 1, file) != 1)
+ {
+ /*
+ * 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_walseg_path(segno));
+ }
+}
+
+/*
+ * Remove temporary file
+ */
+void
+remove_tmp_walseg(XLogSegNo segno, bool update_entry)
+{
+ char *fpath = get_tmp_walseg_path(segno);
+
+ if (unlink(fpath) == 0)
+ pg_log_debug("removed file \"%s\"", fpath);
+ pfree(fpath);
+
+ /* Update entry if requested */
+ if (update_entry)
+ {
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+ Assert(entry != NULL);
+ entry->tmpseg_exists = false;
+ }
+}
+
/*
* Create an astreamer that can read WAL from tar file.
*/
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 7425d386d0c..a472ef59575 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -466,11 +466,50 @@ TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
{
XLogDumpPrivate *private = state->private_data;
int count = required_read_len(private, targetPagePtr, reqLen);
+ XLogSegNo nextSegNo;
if (private->endptr_reached)
return -1;
- /* Read the WAL page from the archive streamer */
+ /*
+ * 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))
+ {
+ 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. */
+ remove_tmp_walseg(nextSegNo, true);
+ }
+
+ 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
+ */
+ if (tmp_walseg_exists(nextSegNo))
+ {
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(nextSegNo);
+ state->seg.ws_file = open(fpath, O_RDONLY | PG_BINARY, 0);
+ pfree(fpath);
+ }
+ }
+
+ /* Continue reading from the open WAL segment, if any */
+ if (state->seg.ws_file >= 0)
+ return WALDumpReadPage(state, targetPagePtr, count, targetPtr,
+ readBuff);
+
+ /* Otherwise, read the WAL page from the archive streamer */
return read_archive_wal_page(private, targetPagePtr, count, readBuff);
}
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
index 54758c3548a..5c1fb1e080a 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -58,4 +58,8 @@ extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
XLogRecPtr targetPagePtr,
Size count, char *readBuff);
+extern char *get_tmp_walseg_path(XLogSegNo segno);
+extern bool tmp_walseg_exists(XLogSegNo segno);
+extern void remove_tmp_walseg(XLogSegNo segno, bool update_entry);
+
#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 443126a9ce6..d5fa1f6d28d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -7,6 +7,7 @@ use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+use List::Util qw(shuffle);
my $tar = $ENV{TAR};
@@ -272,7 +273,7 @@ sub generate_archive
}
closedir $dh;
- @files = sort @files;
+ @files = shuffle @files;
# move into the WAL directory before archiving files
my $cwd = getcwd;
--
2.47.1
v6-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchapplication/octet-stream; name=v6-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchDownload
From d89200d65e3a9cc2e8737ebea850e63c41a6e0e6 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 14:47:43 +0530
Subject: [PATCH v6 6/8] pg_verifybackup: Delay default WAL directory
preparation.
We are not sure whether to parse WAL from a directory or an archive
until the backup format is known. Therefore, we delay preparing the
default WAL directory until the point of parsing. This delay is
harmless, as the WAL directory is not used elsewhere.
---
src/bin/pg_verifybackup/pg_verifybackup.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 8d5befa947f..a502e795b2e 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -285,10 +285,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
@@ -368,6 +364,10 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
+ /* 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 parse the required ranges of WAL records, unless we were told
* not to do so.
--
2.47.1
v6-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchapplication/octet-stream; name=v6-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchDownload
From c9de01ace1794801aa189aebff923a68e93af893 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 24 Jul 2025 16:37:43 +0530
Subject: [PATCH v6 7/8] pg_verifybackup: Rename the wal-directory switch to
wal-path
With previous patches to pg_waldump can now decode WAL directly from
tar files. This means you'll be able to specify a tar archive path
instead of a traditional WAL directory.
To keep things consistent and more versatile, we should also
generalize the input switch for pg_verifybackup. It should accept
either a directory or a tar file path that contains WALs. This change
will also aligning it with the existing manifest-path switch naming.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 2 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 22 +++++++++++-----------
src/bin/pg_verifybackup/po/de.po | 4 ++--
src/bin/pg_verifybackup/po/el.po | 4 ++--
src/bin/pg_verifybackup/po/es.po | 4 ++--
src/bin/pg_verifybackup/po/fr.po | 4 ++--
src/bin/pg_verifybackup/po/it.po | 4 ++--
src/bin/pg_verifybackup/po/ja.po | 4 ++--
src/bin/pg_verifybackup/po/ka.po | 4 ++--
src/bin/pg_verifybackup/po/ko.po | 4 ++--
src/bin/pg_verifybackup/po/ru.po | 4 ++--
src/bin/pg_verifybackup/po/sv.po | 4 ++--
src/bin/pg_verifybackup/po/uk.po | 4 ++--
src/bin/pg_verifybackup/po/zh_CN.po | 4 ++--
src/bin/pg_verifybackup/po/zh_TW.po | 4 ++--
src/bin/pg_verifybackup/t/007_wal.pl | 4 ++--
16 files changed, 40 insertions(+), 40 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index 61c12975e4a..e9b8bfd51b1 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -261,7 +261,7 @@ PostgreSQL documentation
<varlistentry>
<term><option>-w <replaceable class="parameter">path</replaceable></option></term>
- <term><option>--wal-directory=<replaceable class="parameter">path</replaceable></option></term>
+ <term><option>--wal-path=<replaceable class="parameter">path</replaceable></option></term>
<listitem>
<para>
Try to parse WAL files stored in the specified directory, rather than
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index a502e795b2e..9fcd6be004e 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -93,7 +93,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 +126,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 +135,7 @@ 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 *pg_waldump_path = NULL;
DIR *dir;
@@ -221,8 +221,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 */
@@ -365,15 +365,15 @@ main(int argc, char **argv)
verify_backup_checksums(&context);
/* By default, look for the WAL in the backup directory, too. */
- if (wal_directory == NULL)
- wal_directory = psprintf("%s/pg_wal", context.backup_directory);
+ if (wal_path == NULL)
+ wal_path = psprintf("%s/pg_wal", context.backup_directory);
/*
* 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
@@ -1198,7 +1198,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 +1208,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 +1376,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 a9e24931100..9b5cd5898cf 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 3e3f20c67c5..81442f51c17 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 0cb958f3448..7f729fa35ba 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 da8c72f6427..09937966fa7 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 317b0b71e7f..4da68d0074e 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 c910fb236cc..a948959b54f 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 982751984c7..ef2799316a8 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 acdc3da5e02..eaf91ef1e98 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 64005feedfd..7fb0e5ab1f6 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 17240feeb5c..97125838e8c 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 034b9764232..63f8041ab38 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 b7d97c8976d..fb6fcae8b82 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 c1b710b0a36..568f972b0bb 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/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl
index babc4f0a86b..b07f80719b0 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', '--no-parse-wal', $backup_path ],
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: $!";
--
2.47.1
v6-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchapplication/octet-stream; name=v6-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchDownload
From b823bc3086f80e869057ccef0c6e8a3f7c664a9a Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 17 Jul 2025 16:39:36 +0530
Subject: [PATCH v6 8/8] pg_verifybackup: enabled WAL parsing for tar-format
backup
Now that pg_waldump supports decoding from tar archives, we should
leverage this functionality to remove the previous restriction on WAL
parsing for tar-backed formats.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 5 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 66 +++++++++++++------
src/bin/pg_verifybackup/t/002_algorithm.pl | 4 --
src/bin/pg_verifybackup/t/003_corruption.pl | 4 +-
src/bin/pg_verifybackup/t/008_untar.pl | 5 +-
src/bin/pg_verifybackup/t/010_client_untar.pl | 5 +-
6 files changed, 50 insertions(+), 39 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index e9b8bfd51b1..16b50b5a4df 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -36,10 +36,7 @@ PostgreSQL documentation
<literal>backup_manifest</literal> 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 <application>pg_basebackup</application>. However, at present,
- <literal>WAL</literal> verification is supported only for plain-format
- backups. Therefore, if the backup is stored in tar-format, the
- <literal>-n, --no-parse-wal</literal> option should be used.
+ supported by <application>pg_basebackup</application>.
</para>
<para>
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 9fcd6be004e..6915fc7f28e 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);
@@ -136,6 +140,8 @@ main(int argc, char **argv)
bool no_parse_wal = false;
bool quiet = false;
char *wal_path = NULL;
+ char *base_archive_path = NULL;
+ char *wal_archive_path = NULL;
char *pg_waldump_path = NULL;
DIR *dir;
@@ -327,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'.
@@ -346,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.
@@ -364,9 +359,28 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
- /* By default, look for the WAL in the backup directory, too. */
+ /*
+ * 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)
- wal_path = psprintf("%s/pg_wal", context.backup_directory);
+ {
+ 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
@@ -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 <tablespaceoid>.tar file here. */
diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl
index ae16c11bc4d..4f284a9e828 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 1dd60f709cf..f1ebdbb46b4 100644
--- a/src/bin/pg_verifybackup/t/003_corruption.pl
+++ b/src/bin/pg_verifybackup/t/003_corruption.pl
@@ -193,10 +193,8 @@ for my $scenario (@scenario)
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/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl
index bc3d6b352ad..09079a94fee 100644
--- a/src/bin/pg_verifybackup/t/008_untar.pl
+++ b/src/bin/pg_verifybackup/t/008_untar.pl
@@ -47,7 +47,6 @@ my $tsoid = $primary->safe_psql(
SELECT oid FROM pg_tablespace WHERE spcname = 'regress_ts1'));
my $backup_path = $primary->backup_dir . '/server-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -123,14 +122,12 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
rmtree($backup_path);
- rmtree($extract_path);
}
}
diff --git a/src/bin/pg_verifybackup/t/010_client_untar.pl b/src/bin/pg_verifybackup/t/010_client_untar.pl
index b62faeb5acf..5b0e76ee69d 100644
--- a/src/bin/pg_verifybackup/t/010_client_untar.pl
+++ b/src/bin/pg_verifybackup/t/010_client_untar.pl
@@ -32,7 +32,6 @@ print $jf $junk_data;
close $jf;
my $backup_path = $primary->backup_dir . '/client-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -137,13 +136,11 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
- rmtree($extract_path);
rmtree($backup_path);
}
}
--
2.47.1
On Mon, Nov 17, 2025 at 5:51 AM Amul Sul <sulamul@gmail.com> wrote:
On Thu, Nov 6, 2025 at 2:33 PM Amul Sul <sulamul@gmail.com> wrote:
On Mon, Oct 20, 2025 at 8:05 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Oct 16, 2025 at 7:49 AM Amul Sul <sulamul@gmail.com> wrote:
[....]Kindly have a look at the attached version. Thank you !
Attached is the rebased version against the latest master head (e76defbcf09).
Hi Amul, thanks for working on this. I haven't really looked at the
source code deeply (I trust Robert eyes much more than mine on this
one), just skimmed a little bit:
1. As stated earlier, get_tmp_walseg_path() is still vulnerable (it
uses predictable path that could be used by attacker in $TMPDIR)
2. On the usability front:
a. If you do `pg_waldump --path pg_wal.tar -s 0/31000000` it will dump
a lot of WAL records and then print final:
pg_waldump: error: could not find file "000000010000000000000034" in archive
However, with `pg_waldump --path pg_wal.tar -s 0/31000000
--stats=record` (not passing '-e') it will simply bailout without
printing stats and with error:
pg_waldump: error: could not find file "000000010000000000000034" in archive
IMHO, it could print stats if it was capable of getting at least 1 WAL record.
3. The most critical issue for me was the initial lack of error
pass-through from pg_waldump (when used with WALs in tar) to the
pg_verifybackup. Now it works fine, so thanks for this:
a. pg_waldump is capable of discovering missing WALs as requested and
throwing proper return code (good)
$ /usr/pgsql19/bin/pg_waldump --path pg_wal.tar -s 0/31005F70 -e 0/343D2650 -q
pg_waldump: error: could not find file "000000010000000000000034" in archive
$ echo $?
1
$
b. pg_verifybackup now also complains properly with missing WAL inside tar
$ tar --delete -f pg_wal.tar 000000010000000000000032 # simulate loss of file
$ tar -tf pg_wal.tar
000000010000000000000031
archive_status/000000010000000000000031.done
archive_status/000000010000000000000032.done
000000010000000000000033
$ grep Start-LSN backup_manifest
{ "Timeline": 1, "Start-LSN": "0/31005F70", "End-LSN": "0/333D2650" }
$ /usr/pgsql19/bin/pg_verifybackup -P /tmp/basebackup/
791372/791372 kB (100%) verified
pg_waldump: error: could not find file "000000010000000000000032" in archive
pg_verifybackup: error: WAL parsing failed for timeline 1
$ echo $?
1
$
-J.
On Wed, Nov 19, 2025 at 1:50 PM Jakub Wartak
<jakub.wartak@enterprisedb.com> wrote:
On Mon, Nov 17, 2025 at 5:51 AM Amul Sul <sulamul@gmail.com> wrote:
On Thu, Nov 6, 2025 at 2:33 PM Amul Sul <sulamul@gmail.com> wrote:
On Mon, Oct 20, 2025 at 8:05 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Oct 16, 2025 at 7:49 AM Amul Sul <sulamul@gmail.com> wrote:
[....]Kindly have a look at the attached version. Thank you !
Attached is the rebased version against the latest master head (e76defbcf09).
Hi Amul, thanks for working on this. I haven't really looked at the
source code deeply (I trust Robert eyes much more than mine on this
one), just skimmed a little bit:1. As stated earlier, get_tmp_walseg_path() is still vulnerable (it
uses predictable path that could be used by attacker in $TMPDIR)
Yeah, I haven't done anything regarding this since I am unsure of what
should be done and what the risks involved are. I am thinking of
taking Robert's opinion on this.
2. On the usability front:
a. If you do `pg_waldump --path pg_wal.tar -s 0/31000000` it will dump
a lot of WAL records and then print final:
pg_waldump: error: could not find file "000000010000000000000034" in archiveHowever, with `pg_waldump --path pg_wal.tar -s 0/31000000
--stats=record` (not passing '-e') it will simply bailout without
printing stats and with error:
pg_waldump: error: could not find file "000000010000000000000034" in archiveIMHO, it could print stats if it was capable of getting at least 1 WAL record.
The similar behavior in the current pg_waldump when using the --path
option with a WAL directory and a starting LSN. E.g:
$ pg_waldump -s 0/04FE36E0 --path=/tmp/backup/tmp/ --stats=record
pg_waldump: first record is after 0/04FE36E0, at 0/04FE3F90, skipping
over 2224 bytes
pg_waldump: error: could not find file "000000010000000000000009": No
such file or directory
3. The most critical issue for me was the initial lack of error
pass-through from pg_waldump (when used with WALs in tar) to the
pg_verifybackup. Now it works fine, so thanks for this:
Thanks, that was exactly the intention -- to complete pg_verifybackup
for tar-formatted backup verification.
Regards,
Amul
On Mon, Nov 17, 2025 at 10:20 AM Amul Sul <sulamul@gmail.com> wrote:
On Thu, Nov 6, 2025 at 2:33 PM Amul Sul <sulamul@gmail.com> wrote:
On Mon, Oct 20, 2025 at 8:05 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Oct 16, 2025 at 7:49 AM Amul Sul <sulamul@gmail.com> wrote:
[....]Kindly have a look at the attached version. Thank you !
Attached is the updated version. I have fixed an assertion failure
that can occasionally occur with a partial WAL page read.
Regards,
Amul
Attachments:
v7-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchapplication/octet-stream; name=v7-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchDownload
From b09abd6a9fc5493a71285a36417bcc18ac017985 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 6 Nov 2025 13:48:33 +0530
Subject: [PATCH v7 5/8] pg_waldump: Remove the restriction on the order of
archived WAL files.
With previous patch, pg_waldump would stop decoding if WAL files were
not in the required sequence. With this patch, decoding will now
continue. Any WAL file that is out of order will be written to a
temporary location, from which it will be read later. Once a temporary
file has been read, it will be removed.
---
src/bin/pg_waldump/archive_waldump.c | 207 +++++++++++++++++++++++++--
src/bin/pg_waldump/pg_waldump.c | 41 +++++-
src/bin/pg_waldump/pg_waldump.h | 4 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
4 files changed, 243 insertions(+), 12 deletions(-)
diff --git a/src/bin/pg_waldump/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
index 61d6782f9b7..6f87c1ab4a4 100644
--- a/src/bin/pg_waldump/archive_waldump.c
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -17,6 +17,7 @@
#include <unistd.h>
#include "access/xlog_internal.h"
+#include "common/file_perm.h"
#include "common/hashfn.h"
#include "common/logging.h"
#include "fe_utils/simple_list.h"
@@ -27,6 +28,11 @@
*/
#define READ_CHUNK_SIZE (128 * 1024)
+#define TEMP_FILE_PREFIX "waldump.tmp"
+
+/* Temporary exported WAL file directory */
+static char *TmpWalSegDir = NULL;
+
/* Structure for storing the WAL segment data from the archive */
typedef struct ArchivedWALEntry
{
@@ -65,6 +71,11 @@ typedef struct astreamer_waldump
static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
XLogDumpPrivate *privateInfo);
+static void setup_tmpseg_dir(const char *waldir);
+static void cleanup_tmpseg_dir_atexit(void);
+
+static FILE *prepare_tmp_write(XLogSegNo segno);
+static void perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file);
static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
static void astreamer_waldump_content(astreamer *streamer,
@@ -120,10 +131,11 @@ is_archive_file(const char *fname, pg_compress_algorithm *compression)
}
/*
- * Initializes the tar archive reader to read WAL files from the archive,
- * creates a hash table to store them, performs quick existence checks for WAL
- * entries in the archive and retrieves the WAL segment size, and sets up
- * filtering criteria for relevant entries.
+ * Initializes the tar archive reader, creates a hash table for WAL entries,
+ * checks for existing valid WAL segments in the archive file and retrieves the
+ * segment size, and sets up filters for relevant entries. It also configures a
+ * temporary directory for out-of-order WAL data and registers an exit callback
+ * to clean up temporary files.
*/
void
init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
@@ -194,6 +206,13 @@ init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
*/
XLByteToSeg(privateInfo->startptr, privateInfo->startSegNo, WalSegSz);
XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+
+ /*
+ * Setup temporary directory to store WAL segments and set up an exit
+ * callback to remove it upon completion.
+ */
+ setup_tmpseg_dir(waldir);
+ atexit(cleanup_tmpseg_dir_atexit);
}
/*
@@ -369,13 +388,16 @@ read_archive_file(XLogDumpPrivate *privateInfo, Size count)
/*
* Returns the archived WAL entry from the hash table if it exists. Otherwise,
* it invokes the routine to read the archived file and retrieve the entry if
- * it is not already in hash table.
+ * it is not already present in the hash table. If the archive streamer happens
+ * to be reading a WAL from archive file that is not currently needed, that WAL
+ * data is written to a temporary file.
*/
static ArchivedWALEntry *
get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
{
ArchivedWALEntry *entry = NULL;
char fname[MAXFNAMELEN];
+ FILE *write_fp = NULL;
/* Search hash table */
entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
@@ -418,11 +440,32 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
continue;
}
- /* WAL segments must be archived in order */
- pg_log_error("WAL files are not archived in sequential order");
- pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
- segno, entry->segno);
- exit(1);
+ /*
+ * Archive streamer is currently reading a file that isn't the one
+ * asked for, but it's required for a future feature. It should be
+ * written to a temporary location for retrieval when needed.
+ */
+
+ /* Create a temporary file if one does not already exist */
+ if (!entry->tmpseg_exists)
+ {
+ write_fp = prepare_tmp_write(entry->segno);
+ entry->tmpseg_exists = true;
+ }
+
+ /* Flush data from the buffer to the file */
+ perform_tmp_write(entry->segno, &entry->buf, write_fp);
+ resetStringInfo(&entry->buf);
+
+ /*
+ * The change in the current segment entry indicates that the reading
+ * of this file has ended.
+ */
+ if (entry != privateInfo->cur_wal && write_fp != NULL)
+ {
+ fclose(write_fp);
+ write_fp = NULL;
+ }
}
/* Requested WAL segment not found */
@@ -430,6 +473,150 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
pg_fatal("could not find file \"%s\" in archive", fname);
}
+/*
+ * Set up a temporary directory to temporarily store WAL segments.
+ */
+static void
+setup_tmpseg_dir(const char *waldir)
+{
+ /*
+ * Use the directory specified by the TEMDIR environment variable. If it's
+ * not set, use the provided WAL directory to extract WAL file
+ * temporarily.
+ */
+ 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
+cleanup_tmpseg_dir_atexit(void)
+{
+ ArchivedWAL_iterator it;
+ ArchivedWALEntry *entry;
+
+ ArchivedWAL_start_iterate(ArchivedWAL_HTAB, &it);
+ while ((entry = ArchivedWAL_iterate(ArchivedWAL_HTAB, &it)) != NULL)
+ {
+ if (entry->tmpseg_exists)
+ {
+ remove_tmp_walseg(entry->segno, false);
+ entry->tmpseg_exists = false;
+ }
+ }
+}
+
+/*
+ * Generate the temporary WAL file path.
+ *
+ * Note that the caller is responsible to pfree it.
+ */
+char *
+get_tmp_walseg_path(XLogSegNo segno)
+{
+ char *fpath = (char *) palloc(MAXPGPATH);
+
+ snprintf(fpath, MAXPGPATH, "%s/%s.%08X%08X",
+ TmpWalSegDir,
+ TEMP_FILE_PREFIX,
+ (uint32) (segno / XLogSegmentsPerXLogId(WalSegSz)),
+ (uint32) (segno % XLogSegmentsPerXLogId(WalSegSz)));
+
+ return fpath;
+}
+
+/*
+ * Routine to check whether a temporary file exists for the corresponding WAL
+ * segment number.
+ */
+bool
+tmp_walseg_exists(XLogSegNo segno)
+{
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry == NULL)
+ return false;
+
+ return entry->tmpseg_exists;
+}
+
+/*
+ * Create an empty placeholder file and return its handle.
+ */
+static FILE *
+prepare_tmp_write(XLogSegNo segno)
+{
+ FILE *file;
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(segno);
+
+ /* 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_debug("temporarily exporting file \"%s\"", fpath);
+ pfree(fpath);
+
+ return file;
+}
+
+/*
+ * Write buffer data to the given file handle.
+ */
+static void
+perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file)
+{
+ Assert(file);
+
+ errno = 0;
+ if (buf->len > 0 && fwrite(buf->data, buf->len, 1, file) != 1)
+ {
+ /*
+ * 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_walseg_path(segno));
+ }
+}
+
+/*
+ * Remove temporary file
+ */
+void
+remove_tmp_walseg(XLogSegNo segno, bool update_entry)
+{
+ char *fpath = get_tmp_walseg_path(segno);
+
+ if (unlink(fpath) == 0)
+ pg_log_debug("removed file \"%s\"", fpath);
+ pfree(fpath);
+
+ /* Update entry if requested */
+ if (update_entry)
+ {
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+ Assert(entry != NULL);
+ entry->tmpseg_exists = false;
+ }
+}
+
/*
* Create an astreamer that can read WAL from tar file.
*/
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 02ad141e44a..4c5974a6ae1 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -466,11 +466,50 @@ TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
{
XLogDumpPrivate *private = state->private_data;
int count = required_read_len(private, targetPagePtr, reqLen);
+ XLogSegNo nextSegNo;
if (private->endptr_reached)
return -1;
- /* Read the WAL page from the archive streamer */
+ /*
+ * 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))
+ {
+ 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. */
+ remove_tmp_walseg(nextSegNo, true);
+ }
+
+ 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
+ */
+ if (tmp_walseg_exists(nextSegNo))
+ {
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(nextSegNo);
+ state->seg.ws_file = open(fpath, O_RDONLY | PG_BINARY, 0);
+ pfree(fpath);
+ }
+ }
+
+ /* Continue reading from the open WAL segment, if any */
+ if (state->seg.ws_file >= 0)
+ return WALDumpReadPage(state, targetPagePtr, count, targetPtr,
+ readBuff);
+
+ /* Otherwise, read the WAL page from the archive streamer */
return read_archive_wal_page(private, targetPagePtr, count, readBuff);
}
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
index 54758c3548a..5c1fb1e080a 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -58,4 +58,8 @@ extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
XLogRecPtr targetPagePtr,
Size count, char *readBuff);
+extern char *get_tmp_walseg_path(XLogSegNo segno);
+extern bool tmp_walseg_exists(XLogSegNo segno);
+extern void remove_tmp_walseg(XLogSegNo segno, bool update_entry);
+
#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 443126a9ce6..d5fa1f6d28d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -7,6 +7,7 @@ use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+use List::Util qw(shuffle);
my $tar = $ENV{TAR};
@@ -272,7 +273,7 @@ sub generate_archive
}
closedir $dh;
- @files = sort @files;
+ @files = shuffle @files;
# move into the WAL directory before archiving files
my $cwd = getcwd;
--
2.47.1
v7-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchapplication/octet-stream; name=v7-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchDownload
From 7e2523b0891c9911f64c35f736db24e3310e2090 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 14:47:43 +0530
Subject: [PATCH v7 6/8] pg_verifybackup: Delay default WAL directory
preparation.
We are not sure whether to parse WAL from a directory or an archive
until the backup format is known. Therefore, we delay preparing the
default WAL directory until the point of parsing. This delay is
harmless, as the WAL directory is not used elsewhere.
---
src/bin/pg_verifybackup/pg_verifybackup.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 8d5befa947f..a502e795b2e 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -285,10 +285,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
@@ -368,6 +364,10 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
+ /* 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 parse the required ranges of WAL records, unless we were told
* not to do so.
--
2.47.1
v7-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchapplication/octet-stream; name=v7-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchDownload
From 705fb42e96663b692c5b51650646cf0361b8083e Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 24 Jul 2025 16:37:43 +0530
Subject: [PATCH v7 7/8] pg_verifybackup: Rename the wal-directory switch to
wal-path
With previous patches to pg_waldump can now decode WAL directly from
tar files. This means you'll be able to specify a tar archive path
instead of a traditional WAL directory.
To keep things consistent and more versatile, we should also
generalize the input switch for pg_verifybackup. It should accept
either a directory or a tar file path that contains WALs. This change
will also aligning it with the existing manifest-path switch naming.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 2 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 22 +++++++++++-----------
src/bin/pg_verifybackup/po/de.po | 4 ++--
src/bin/pg_verifybackup/po/el.po | 4 ++--
src/bin/pg_verifybackup/po/es.po | 4 ++--
src/bin/pg_verifybackup/po/fr.po | 4 ++--
src/bin/pg_verifybackup/po/it.po | 4 ++--
src/bin/pg_verifybackup/po/ja.po | 4 ++--
src/bin/pg_verifybackup/po/ka.po | 4 ++--
src/bin/pg_verifybackup/po/ko.po | 4 ++--
src/bin/pg_verifybackup/po/ru.po | 4 ++--
src/bin/pg_verifybackup/po/sv.po | 4 ++--
src/bin/pg_verifybackup/po/uk.po | 4 ++--
src/bin/pg_verifybackup/po/zh_CN.po | 4 ++--
src/bin/pg_verifybackup/po/zh_TW.po | 4 ++--
src/bin/pg_verifybackup/t/007_wal.pl | 4 ++--
16 files changed, 40 insertions(+), 40 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index 61c12975e4a..e9b8bfd51b1 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -261,7 +261,7 @@ PostgreSQL documentation
<varlistentry>
<term><option>-w <replaceable class="parameter">path</replaceable></option></term>
- <term><option>--wal-directory=<replaceable class="parameter">path</replaceable></option></term>
+ <term><option>--wal-path=<replaceable class="parameter">path</replaceable></option></term>
<listitem>
<para>
Try to parse WAL files stored in the specified directory, rather than
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index a502e795b2e..9fcd6be004e 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -93,7 +93,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 +126,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 +135,7 @@ 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 *pg_waldump_path = NULL;
DIR *dir;
@@ -221,8 +221,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 */
@@ -365,15 +365,15 @@ main(int argc, char **argv)
verify_backup_checksums(&context);
/* By default, look for the WAL in the backup directory, too. */
- if (wal_directory == NULL)
- wal_directory = psprintf("%s/pg_wal", context.backup_directory);
+ if (wal_path == NULL)
+ wal_path = psprintf("%s/pg_wal", context.backup_directory);
/*
* 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
@@ -1198,7 +1198,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 +1208,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 +1376,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 a9e24931100..9b5cd5898cf 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 3e3f20c67c5..81442f51c17 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 0cb958f3448..7f729fa35ba 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 da8c72f6427..09937966fa7 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 317b0b71e7f..4da68d0074e 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 c910fb236cc..a948959b54f 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 982751984c7..ef2799316a8 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 acdc3da5e02..eaf91ef1e98 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 64005feedfd..7fb0e5ab1f6 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 17240feeb5c..97125838e8c 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 034b9764232..63f8041ab38 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 b7d97c8976d..fb6fcae8b82 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 c1b710b0a36..568f972b0bb 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/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl
index babc4f0a86b..b07f80719b0 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', '--no-parse-wal', $backup_path ],
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: $!";
--
2.47.1
v7-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchapplication/octet-stream; name=v7-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchDownload
From 280432691a9c98b1006d85769ee8e5a5869e55c8 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 24 Jun 2025 11:33:20 +0530
Subject: [PATCH v7 1/8] Refactor: pg_waldump: Move some declarations to new
pg_waldump.h
This change prepares for a second source file in this directory to
support reading WAL from tar files. Common structures, declarations,
and functions are being exported through this include file so
they can be used in both files.
---
src/bin/pg_waldump/pg_waldump.c | 11 ++---------
src/bin/pg_waldump/pg_waldump.h | 27 +++++++++++++++++++++++++++
2 files changed, 29 insertions(+), 9 deletions(-)
create mode 100644 src/bin/pg_waldump/pg_waldump.h
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index c6d6ba79e44..5846ee24f46 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,19 +40,11 @@
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};
-typedef struct XLogDumpPrivate
-{
- TimeLineID timeline;
- XLogRecPtr startptr;
- XLogRecPtr endptr;
- bool endptr_reached;
-} XLogDumpPrivate;
-
typedef struct XLogDumpConfig
{
/* display options */
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
new file mode 100644
index 00000000000..9e62b64ead5
--- /dev/null
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -0,0 +1,27 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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"
+
+extern int WalSegSz;
+
+/* Contains the necessary information to drive WAL decoding */
+typedef struct XLogDumpPrivate
+{
+ TimeLineID timeline;
+ XLogRecPtr startptr;
+ XLogRecPtr endptr;
+ bool endptr_reached;
+} XLogDumpPrivate;
+
+#endif /* end of PG_WALDUMP_H */
--
2.47.1
v7-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchapplication/octet-stream; name=v7-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchDownload
From 11840029bc52e927858da7be1f91ece9b680d486 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 26 Jun 2025 11:42:53 +0530
Subject: [PATCH v7 2/8] Refactor: pg_waldump: Separate logic used to calculate
the required read size.
This refactoring prepares the codebase for an upcoming patch that will
support reading WAL from tar files. The logic for calculating the
required read size has been updated to handle both normal WAL files
and WAL files located inside a tar archive.
---
src/bin/pg_waldump/pg_waldump.c | 39 ++++++++++++++++++++++-----------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 5846ee24f46..0dc28ea360c 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -326,6 +326,29 @@ identify_target_directory(char *directory, char *fname)
return NULL; /* not reached */
}
+/* 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 (XLogRecPtrIsValid(private->endptr))
+ {
+ 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,
@@ -383,21 +406,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 (XLogRecPtrIsValid(private->endptr))
- {
- 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))
--
2.47.1
v7-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchapplication/octet-stream; name=v7-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchDownload
From 62a143b267d60d3c98b5063c3874c4c22135d1c0 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 30 Jul 2025 12:43:30 +0530
Subject: [PATCH v7 3/8] Refactor: pg_waldump: Restructure TAP tests.
Restructured some tests to run inside a loop, facilitating their
re-execution for decoding WAL from tar archives.
---
src/bin/pg_waldump/t/001_basic.pl | 123 ++++++++++++++++--------------
1 file changed, 67 insertions(+), 56 deletions(-)
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index f26d75e01cf..1b712e8d74d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -198,28 +198,6 @@ command_like(
],
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 +205,6 @@ command_like(
],
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.
@@ -272,7 +241,6 @@ sub test_pg_waldump
my $result = IPC::Run::run [
'pg_waldump',
- '--path' => $node->data_dir,
'--start' => $start_lsn,
'--end' => $end_lsn,
@opts
@@ -288,38 +256,81 @@ sub test_pg_waldump
my @lines;
-@lines = test_pg_waldump;
-is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
+my @scenario = (
+ {
+ 'path' => $node->data_dir
+ });
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenario)
+{
+ my $path = $scenario->{'path'};
-@lines = test_pg_waldump('--fullpage');
-is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
+ SKIP:
+ {
+ 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');
-@lines = test_pg_waldump('--stats');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+ command_fails_like(
+ [
+ 'pg_waldump', '--quiet',
+ '--path' => $path,
+ '--start' => $start_lsn
+ ],
+ qr/error: error in WAL record at/,
+ 'errors are shown with --quiet');
-@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('--path' => $path);
+ is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
-@lines = test_pg_waldump('--rmgr' => 'Btree');
-is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+ @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ is(@lines, 6, 'limit option observed');
-@lines = test_pg_waldump('--fork' => 'init');
-is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+ @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
-@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('--path' => $path, '--stats');
+ like($lines[0], qr/WAL statistics/, "statistics on stdout");
+ is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
-@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');
+ @lines = test_pg_waldump('--path' => $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' => $path, '--rmgr' => 'Btree');
+ is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+
+ @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+
+ @lines = test_pg_waldump('--path' => $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' => $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');
+ }
+}
done_testing();
--
2.47.1
v7-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchapplication/octet-stream; name=v7-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchDownload
From 8a2a15082f43f2cb4c30913f89f749b606ffee13 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 5 Nov 2025 15:40:36 +0530
Subject: [PATCH v7 4/8] pg_waldump: Add support for archived WAL decoding.
pg_waldump can now accept the path to a tar archive containing WAL
files and decode them. This feature was added primarily for
pg_verifybackup, which previously disabled WAL parsing for
tar-formatted backups.
Note that this patch requires that the WAL files within the archive be
in sequential order; an error will be reported otherwise. The next
patch is planned to remove this restriction.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/Makefile | 7 +-
src/bin/pg_waldump/archive_waldump.c | 584 +++++++++++++++++++++++++++
src/bin/pg_waldump/meson.build | 4 +-
src/bin/pg_waldump/pg_waldump.c | 215 +++++++---
src/bin/pg_waldump/pg_waldump.h | 36 +-
src/bin/pg_waldump/t/001_basic.pl | 84 +++-
src/tools/pgindent/typedefs.list | 3 +
8 files changed, 865 insertions(+), 76 deletions(-)
create mode 100644 src/bin/pg_waldump/archive_waldump.c
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index ce23add5577..d004bb0f67e 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -141,13 +141,17 @@ PostgreSQL documentation
<term><option>--path=<replaceable>path</replaceable></option></term>
<listitem>
<para>
- Specifies a directory to search for WAL segment files or a
- directory with a <literal>pg_wal</literal> subdirectory that
+ Specifies a tar archive or a directory to search for WAL segment files
+ or a directory with a <literal>pg_wal</literal> subdirectory that
contains such files. The default is to search in the current
directory, the <literal>pg_wal</literal> subdirectory of the
current directory, and the <literal>pg_wal</literal> subdirectory
of <envar>PGDATA</envar>.
</para>
+ <para>
+ If a tar archive is provided, its WAL segment files must be in
+ sequential order; otherwise, an error will be reported.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile
index 4c1ee649501..05ac5763a57 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 \
+ archive_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/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
new file mode 100644
index 00000000000..61d6782f9b7
--- /dev/null
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -0,0 +1,584 @@
+/*-------------------------------------------------------------------------
+ *
+ * archive_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/archive_waldump.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <unistd.h>
+
+#include "access/xlog_internal.h"
+#include "common/hashfn.h"
+#include "common/logging.h"
+#include "fe_utils/simple_list.h"
+#include "pg_waldump.h"
+
+/*
+ * How many bytes should we try to read from a file at once?
+ */
+#define READ_CHUNK_SIZE (128 * 1024)
+
+/* Structure for storing the WAL segment data from the archive */
+typedef struct ArchivedWALEntry
+{
+ uint32 status; /* hash status */
+ XLogSegNo segno; /* hash key: WAL segment number */
+ TimeLineID timeline; /* timeline of this wal file */
+
+ StringInfoData buf;
+ bool tmpseg_exists; /* spill file exists? */
+
+ int total_read; /* total read of this WAL segment, including
+ * buffered and temporarily written data */
+} ArchivedWALEntry;
+
+#define SH_PREFIX ArchivedWAL
+#define SH_ELEMENT_TYPE ArchivedWALEntry
+#define SH_KEY_TYPE XLogSegNo
+#define SH_KEY segno
+#define SH_HASH_KEY(tb, key) murmurhash64((uint64) key)
+#define SH_EQUAL(tb, a, b) (a == b)
+#define SH_GET_HASH(tb, a) a->hash
+#define SH_SCOPE static inline
+#define SH_RAW_ALLOCATOR pg_malloc0
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
+
+static ArchivedWAL_hash *ArchivedWAL_HTAB = NULL;
+
+typedef struct astreamer_waldump
+{
+ astreamer base;
+ XLogDumpPrivate *privateInfo;
+} astreamer_waldump;
+
+static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
+static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
+ XLogDumpPrivate *privateInfo);
+
+static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
+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_wal_file(astreamer_waldump *mystreamer,
+ astreamer_member *member,
+ XLogSegNo *curSegNo,
+ TimeLineID *curTimeline);
+
+static const astreamer_ops astreamer_waldump_ops = {
+ .content = astreamer_waldump_content,
+ .finalize = astreamer_waldump_finalize,
+ .free = astreamer_waldump_free
+};
+
+/*
+ * Returns true if the given file is a tar archive and outputs its compression
+ * algorithm.
+ */
+bool
+is_archive_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;
+}
+
+/*
+ * Initializes the tar archive reader to read WAL files from the archive,
+ * creates a hash table to store them, performs quick existence checks for WAL
+ * entries in the archive and retrieves the WAL segment size, and sets up
+ * filtering criteria for relevant entries.
+ */
+void
+init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
+ pg_compress_algorithm compression)
+{
+ int fd;
+ astreamer *streamer;
+ ArchivedWALEntry *entry = NULL;
+ XLogLongPageHeader longhdr;
+
+ /* Open tar archive and store its file descriptor */
+ fd = open_file_in_directory(waldir, privateInfo->archive_name);
+
+ if (fd < 0)
+ pg_fatal("could not open file \"%s\"", privateInfo->archive_name);
+
+ privateInfo->archive_fd = fd;
+
+ streamer = astreamer_waldump_new(privateInfo);
+
+ /* 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);
+
+ privateInfo->archive_streamer = streamer;
+
+ /* Hash table storing WAL entries read from the archive */
+ ArchivedWAL_HTAB = ArchivedWAL_create(16, NULL);
+
+ /*
+ * Verify that the archive contains valid WAL files and fetch WAL segment
+ * size
+ */
+ while (entry == NULL || entry->buf.len < XLOG_BLCKSZ)
+ {
+ if (read_archive_file(privateInfo, XLOG_BLCKSZ) == 0)
+ pg_fatal("could not find WAL in \"%s\" archive",
+ privateInfo->archive_name);
+
+ entry = privateInfo->cur_wal;
+ }
+
+ /* Set WalSegSz if WAL data is successfully read */
+ longhdr = (XLogLongPageHeader) entry->buf.data;
+
+ WalSegSz = longhdr->xlp_seg_size;
+
+ if (!IsValidWalSegSize(WalSegSz))
+ {
+ pg_log_error(ngettext("invalid WAL segment size in WAL file from archive \"%s\" (%d byte)",
+ "invalid WAL segment size in WAL file from archive \"%s\" (%d bytes)",
+ WalSegSz),
+ privateInfo->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);
+ }
+
+ /*
+ * With the WAL segment size available, we can now initialize the
+ * dependent start and end segment numbers.
+ */
+ XLByteToSeg(privateInfo->startptr, privateInfo->startSegNo, WalSegSz);
+ XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+}
+
+/*
+ * Release the archive streamer chain and close the archive file.
+ */
+void
+free_archive_reader(XLogDumpPrivate *privateInfo)
+{
+ /*
+ * 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(privateInfo->archive_streamer);
+
+ /* Close the file. */
+ if (close(privateInfo->archive_fd) != 0)
+ pg_log_error("could not close file \"%s\": %m",
+ privateInfo->archive_name);
+}
+
+/*
+ * Copies WAL data from astreamer to readBuff; if unavailable, fetches more
+ * from the tar archive via astreamer.
+ */
+int
+read_archive_wal_page(XLogDumpPrivate *privateInfo, XLogRecPtr targetPagePtr,
+ Size count, char *readBuff)
+{
+ char *p = readBuff;
+ Size nbytes = count;
+ XLogRecPtr recptr = targetPagePtr;
+ XLogSegNo segno;
+ ArchivedWALEntry *entry;
+
+ XLByteToSeg(targetPagePtr, segno, WalSegSz);
+ entry = get_archive_wal_entry(segno, privateInfo);
+
+ while (nbytes > 0)
+ {
+ char *buf = entry->buf.data;
+ int len = entry->buf.len;
+
+ /* WAL record range that the buffer contains */
+ XLogRecPtr endPtr;
+ XLogRecPtr startPtr;
+
+ XLogSegNoOffsetToRecPtr(entry->segno, entry->total_read,
+ WalSegSz, endPtr);
+ startPtr = endPtr - len;
+
+ /*
+ * pg_waldump may request to re-read the currently active page, but
+ * never a page older than the current one. Therefore, any fully
+ * consumed WAL data preceding the current page can be safely
+ * discarded.
+ */
+ if (recptr >= endPtr)
+ {
+ /* Discard the buffered data */
+ resetStringInfo(&entry->buf);
+ len = 0;
+
+ /*
+ * Push back the partial page data for the current page to the
+ * buffer, ensuring it remains available for re-reading if
+ * requested.
+ */
+ if (p > readBuff)
+ {
+ Assert((count - nbytes) > 0);
+ appendBinaryStringInfo(&entry->buf, readBuff, count - nbytes);
+ }
+ }
+
+ if (len > 0 && recptr > startPtr)
+ {
+ int skipBytes = 0;
+
+ /*
+ * The required offset is not at the start of the 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 the reading page is in the buffer */
+ Assert(recptr >= startPtr && recptr < endPtr);
+
+ memcpy(p, buf, readBytes);
+
+ /* Update state for read */
+ nbytes -= readBytes;
+ p += readBytes;
+ recptr += readBytes;
+ }
+ else
+ {
+ /*
+ * Fetch more data; raise an error if it's not the current segment
+ * being read by the archive streamer or if reading of the
+ * archived file has finished.
+ */
+ if (privateInfo->cur_wal != entry ||
+ read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ {
+ char fname[MAXFNAMELEN];
+
+ XLogFileName(fname, privateInfo->timeline, entry->segno,
+ WalSegSz);
+ pg_fatal("could not read file \"%s\" from archive \"%s\": read %lld of %lld",
+ fname, privateInfo->archive_name,
+ (long long int) count - nbytes,
+ (long long int) nbytes);
+ }
+ }
+ }
+
+ /*
+ * Should have either have successfully read all the requested bytes or
+ * reported a failure before this point.
+ */
+ Assert(nbytes == 0);
+
+ /*
+ * NB: We return the fixed value provided as input. Although we could
+ * return a boolean since we either successfully read the WAL page or
+ * raise an error, but the caller expects this value to be returned. The
+ * routine that reads WAL pages from the physical WAL file follows the
+ * same convention.
+ */
+ return count;
+}
+
+/*
+ * Reads the archive file and passes it to the archive streamer for
+ * decompression.
+ */
+static int
+read_archive_file(XLogDumpPrivate *privateInfo, Size count)
+{
+ int rc;
+ char *buffer;
+
+ buffer = pg_malloc(READ_CHUNK_SIZE * sizeof(uint8));
+
+ rc = read(privateInfo->archive_fd, buffer, count);
+ if (rc < 0)
+ pg_fatal("could not read file \"%s\": %m",
+ privateInfo->archive_name);
+
+ /*
+ * Decompress (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;
+}
+
+/*
+ * Returns the archived WAL entry from the hash table if it exists. Otherwise,
+ * it invokes the routine to read the archived file and retrieve the entry if
+ * it is not already in hash table.
+ */
+static ArchivedWALEntry *
+get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
+{
+ ArchivedWALEntry *entry = NULL;
+ char fname[MAXFNAMELEN];
+
+ /* Search hash table */
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry != NULL)
+ return entry;
+
+ /* Needed WAL yet to be decoded from archive, do the same */
+ while (1)
+ {
+ entry = privateInfo->cur_wal;
+
+ /* Fetch more data */
+ if (entry == NULL || entry->buf.len == 0)
+ {
+ if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ break; /* archive file ended */
+ }
+
+ /*
+ * Either, here for the first time, or the archived streamer is
+ * reading a non-WAL file or an irrelevant WAL file.
+ */
+ if (entry == NULL)
+ continue;
+
+ /* Found the required entry */
+ if (entry->segno == segno)
+ return entry;
+
+ /*
+ * Ignore if the timeline is different or the current segment is not
+ * the desired one.
+ */
+ if (privateInfo->timeline != entry->timeline ||
+ privateInfo->startSegNo > entry->segno ||
+ privateInfo->endSegNo < entry->segno)
+ {
+ privateInfo->cur_wal = NULL;
+ continue;
+ }
+
+ /* WAL segments must be archived in order */
+ pg_log_error("WAL files are not archived in sequential order");
+ pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
+ segno, entry->segno);
+ exit(1);
+ }
+
+ /* Requested WAL segment not found */
+ XLogFileName(fname, privateInfo->timeline, segno, WalSegSz);
+ pg_fatal("could not find file \"%s\" in archive", fname);
+}
+
+/*
+ * Create an astreamer that can read WAL from tar file.
+ */
+static astreamer *
+astreamer_waldump_new(XLogDumpPrivate *privateInfo)
+{
+ astreamer_waldump *streamer;
+
+ streamer = palloc0(sizeof(astreamer_waldump));
+ *((const astreamer_ops **) &streamer->base.bbs_ops) =
+ &astreamer_waldump_ops;
+
+ streamer->privateInfo = privateInfo;
+
+ return &streamer->base;
+}
+
+/*
+ * Main entry point of the archive streamer for reading WAL data from a tar
+ * file. If a member is identified as a valid WAL file, a hash entry is created
+ * for it, and its contents are copied into that entry's buffer, making them
+ * accessible to the decoding routine.
+ */
+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:
+ {
+ XLogSegNo segno;
+ TimeLineID timeline;
+ ArchivedWALEntry *entry;
+ bool found;
+
+ pg_log_debug("reading \"%s\"", member->pathname);
+
+ if (!member_is_wal_file(mystreamer, member,
+ &segno, &timeline))
+ break;
+
+ entry = ArchivedWAL_insert(ArchivedWAL_HTAB, segno, &found);
+
+ /*
+ * Shouldn't happen, but if it does, simply ignore the
+ * duplicate WAL file.
+ */
+ if (found)
+ {
+ pg_log_warning("ignoring duplicate WAL file found in archive: \"%s\"",
+ member->pathname);
+ break;
+ }
+
+ initStringInfo(&entry->buf);
+ entry->timeline = timeline;
+ entry->total_read = 0;
+
+ privateInfo->cur_wal = entry;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_CONTENTS:
+ if (privateInfo->cur_wal)
+ {
+ appendBinaryStringInfo(&privateInfo->cur_wal->buf, data, len);
+ privateInfo->cur_wal->total_read += len;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_TRAILER:
+ privateInfo->cur_wal = NULL;
+ 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)
+{
+ Assert(streamer->bbs_next == NULL);
+ pfree(streamer);
+}
+
+/*
+ * Returns true if the archive member name matches the WAL naming format. If
+ * successful, it also outputs the WAL segment number, and timeline.
+ */
+static bool
+member_is_wal_file(astreamer_waldump *mystreamer, astreamer_member *member,
+ XLogSegNo *curSegNo, TimeLineID *curTimeline)
+{
+ 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;
+
+ /*
+ * XXX: On some systems (e.g., OpenBSD), the tar utility includes
+ * PaxHeaders when creating an archive. These are special entries that
+ * store extended metadata for the file entry immediately following them,
+ * and they share the exact same name as that file.
+ */
+ if (strstr(member->pathname, "PaxHeaders."))
+ return false;
+
+ /* Parse position from file */
+ XLogFromFileName(fname, &timeline, &segNo, WalSegSz);
+
+ *curSegNo = segNo;
+ *curTimeline = timeline;
+
+ return true;
+}
diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build
index 937e0d68841..da00746587c 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',
+ 'archive_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 0dc28ea360c..02ad141e44a 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -177,7 +177,7 @@ split_path(const char *path, char **dir, char **fname)
*
* return a read only fd
*/
-static int
+int
open_file_in_directory(const char *directory, const char *fname)
{
int fd = -1;
@@ -436,6 +436,44 @@ 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);
+
+ if (private->endptr_reached)
+ return -1;
+
+ /* Read the WAL page from the archive streamer */
+ return read_archive_wal_page(private, targetPagePtr, count, readBuff);
+}
+
/*
* Boolean to return whether the given WAL record matches a specific relation
* and optionally block.
@@ -773,8 +811,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"
@@ -806,7 +844,10 @@ main(int argc, char **argv)
XLogRecord *record;
XLogRecPtr first_record;
char *waldir = NULL;
+ char *walpath = NULL;
char *errormsg;
+ bool is_archive = false;
+ pg_compress_algorithm compression;
static struct option long_options[] = {
{"bkp-details", no_argument, NULL, 'b'},
@@ -938,7 +979,7 @@ main(int argc, char **argv)
}
break;
case 'p':
- waldir = pg_strdup(optarg);
+ walpath = pg_strdup(optarg);
break;
case 'q':
config.quiet = true;
@@ -1102,10 +1143,20 @@ main(int argc, char **argv)
goto bad_argument;
}
- if (waldir != NULL)
+ if (walpath != NULL)
{
+ /* validate path points to tar archive */
+ if (is_archive_file(walpath, &compression))
+ {
+ char *fname = NULL;
+
+ split_path(walpath, &waldir, &fname);
+
+ private.archive_name = fname;
+ is_archive = 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;
@@ -1123,6 +1174,17 @@ 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_archive)
+ {
+ 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)
@@ -1133,69 +1195,77 @@ main(int argc, char **argv)
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 (!XLogRecPtrIsValid(private.startptr))
- XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr);
- else if (!XLByteInSeg(private.startptr, segno, WalSegSz))
+ if (fname != NULL && is_archive_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_archive = true;
}
-
- /* no second file specified, set end position */
- if (!(optind + 1 < argc) && !XLogRecPtrIsValid(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(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, &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 (!XLogRecPtrIsValid(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 (!XLogRecPtrIsValid(private.endptr))
- XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz,
- private.endptr);
+ /* no second file specified, set end position */
+ if (!(optind + 1 < argc) && !XLogRecPtrIsValid(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 (!XLogRecPtrIsValid(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_archive)
+ waldir = identify_target_directory(walpath, NULL);
/* we don't know what to print */
if (!XLogRecPtrIsValid(private.startptr))
@@ -1207,12 +1277,36 @@ 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_archive)
+ {
+ /*
+ * A NULL WAL directory indicates that the archive file is located
+ * in the current working directory of the pg_waldump execution
+ */
+ waldir = waldir ? pg_strdup(waldir) : pg_strdup(".");
+
+ /* Set up for reading tar file */
+ init_archive_reader(&private, waldir, compression);
+
+ /* 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");
@@ -1321,6 +1415,9 @@ main(int argc, char **argv)
XLogReaderFree(xlogreader_state);
+ if (is_archive)
+ free_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
index 9e62b64ead5..54758c3548a 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -12,9 +12,13 @@
#define PG_WALDUMP_H
#include "access/xlogdefs.h"
+#include "fe_utils/astreamer.h"
extern int WalSegSz;
+/* Forward declaration */
+struct ArchivedWALEntry;
+
/* Contains the necessary information to drive WAL decoding */
typedef struct XLogDumpPrivate
{
@@ -22,6 +26,36 @@ typedef struct XLogDumpPrivate
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;
+
+ /* What the archive streamer is currently reading */
+ struct ArchivedWALEntry *cur_wal;
+
+ /*
+ * Although these values can be easily derived from startptr and endptr,
+ * doing so repeatedly for each archived member would be inefficient, as
+ * it would involve recalculating and filtering out irrelevant WAL
+ * segments.
+ */
+ XLogSegNo startSegNo;
+ XLogSegNo endSegNo;
} XLogDumpPrivate;
-#endif /* end of PG_WALDUMP_H */
+extern int open_file_in_directory(const char *directory, const char *fname);
+
+extern bool is_archive_file(const char *fname,
+ pg_compress_algorithm *compression);
+extern void init_archive_reader(XLogDumpPrivate *privateInfo,
+ const char *waldir,
+ pg_compress_algorithm compression);
+extern void free_archive_reader(XLogDumpPrivate *privateInfo);
+extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
+ XLogRecPtr targetPagePtr,
+ Size count, char *readBuff);
+
+#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 1b712e8d74d..443126a9ce6 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -3,10 +3,13 @@
use strict;
use warnings FATAL => 'all';
+use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $tar = $ENV{TAR};
+
program_help_ok('pg_waldump');
program_version_ok('pg_waldump');
program_options_handling_ok('pg_waldump');
@@ -235,7 +238,7 @@ command_like(
sub test_pg_waldump
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my @opts = @_;
+ my ($path, @opts) = @_;
my ($stdout, $stderr);
@@ -243,6 +246,7 @@ sub test_pg_waldump
'pg_waldump',
'--start' => $start_lsn,
'--end' => $end_lsn,
+ '--path' => $path,
@opts
],
'>' => \$stdout,
@@ -254,11 +258,50 @@ sub test_pg_waldump
return @lines;
}
-my @lines;
+# 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 = sort @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
+ '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)
@@ -267,6 +310,19 @@ for my $scenario (@scenario)
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/,
@@ -298,38 +354,42 @@ for my $scenario (@scenario)
qr/error: error in WAL record at/,
'errors are shown with --quiet');
- @lines = test_pg_waldump('--path' => $path);
+ my @lines;
+ @lines = test_pg_waldump($path);
is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
- @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ @lines = test_pg_waldump($path, '--limit' => 6);
is(@lines, 6, 'limit option observed');
- @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ @lines = test_pg_waldump($path, '--fullpage');
is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
- @lines = test_pg_waldump('--path' => $path, '--stats');
+ @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' => $path, '--stats=record');
+ @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' => $path, '--rmgr' => 'Btree');
+ @lines = test_pg_waldump($path, '--rmgr' => 'Btree');
is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
- @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ @lines = test_pg_waldump($path, '--fork' => 'init');
is(grep(!/fork init/, @lines), 0, 'only init fork lines');
- @lines = test_pg_waldump('--path' => $path,
+ @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' => $path,
+ @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'};
}
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c751c25a04d..c38a1c3808b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -139,6 +139,8 @@ ArchiveOpts
ArchiveShutdownCB
ArchiveStartupCB
ArchiveStreamState
+ArchivedWALEntry
+ArchivedWAL_hash
ArchiverOutput
ArchiverStage
ArrayAnalyzeExtraData
@@ -3465,6 +3467,7 @@ astreamer_recovery_injector
astreamer_tar_archiver
astreamer_tar_parser
astreamer_verify
+astreamer_waldump
astreamer_zstd_frame
auth_password_hook_typ
autovac_table
--
2.47.1
v7-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchapplication/octet-stream; name=v7-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchDownload
From b0eae9adecb56bf634a9193910b702712f6b72dc Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 17 Jul 2025 16:39:36 +0530
Subject: [PATCH v7 8/8] pg_verifybackup: enabled WAL parsing for tar-format
backup
Now that pg_waldump supports decoding from tar archives, we should
leverage this functionality to remove the previous restriction on WAL
parsing for tar-backed formats.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 5 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 66 +++++++++++++------
src/bin/pg_verifybackup/t/002_algorithm.pl | 4 --
src/bin/pg_verifybackup/t/003_corruption.pl | 4 +-
src/bin/pg_verifybackup/t/008_untar.pl | 5 +-
src/bin/pg_verifybackup/t/010_client_untar.pl | 5 +-
6 files changed, 50 insertions(+), 39 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index e9b8bfd51b1..16b50b5a4df 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -36,10 +36,7 @@ PostgreSQL documentation
<literal>backup_manifest</literal> 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 <application>pg_basebackup</application>. However, at present,
- <literal>WAL</literal> verification is supported only for plain-format
- backups. Therefore, if the backup is stored in tar-format, the
- <literal>-n, --no-parse-wal</literal> option should be used.
+ supported by <application>pg_basebackup</application>.
</para>
<para>
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 9fcd6be004e..6915fc7f28e 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);
@@ -136,6 +140,8 @@ main(int argc, char **argv)
bool no_parse_wal = false;
bool quiet = false;
char *wal_path = NULL;
+ char *base_archive_path = NULL;
+ char *wal_archive_path = NULL;
char *pg_waldump_path = NULL;
DIR *dir;
@@ -327,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'.
@@ -346,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.
@@ -364,9 +359,28 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
- /* By default, look for the WAL in the backup directory, too. */
+ /*
+ * 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)
- wal_path = psprintf("%s/pg_wal", context.backup_directory);
+ {
+ 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
@@ -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 <tablespaceoid>.tar file here. */
diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl
index ae16c11bc4d..4f284a9e828 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 1dd60f709cf..f1ebdbb46b4 100644
--- a/src/bin/pg_verifybackup/t/003_corruption.pl
+++ b/src/bin/pg_verifybackup/t/003_corruption.pl
@@ -193,10 +193,8 @@ for my $scenario (@scenario)
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/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl
index bc3d6b352ad..09079a94fee 100644
--- a/src/bin/pg_verifybackup/t/008_untar.pl
+++ b/src/bin/pg_verifybackup/t/008_untar.pl
@@ -47,7 +47,6 @@ my $tsoid = $primary->safe_psql(
SELECT oid FROM pg_tablespace WHERE spcname = 'regress_ts1'));
my $backup_path = $primary->backup_dir . '/server-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -123,14 +122,12 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
rmtree($backup_path);
- rmtree($extract_path);
}
}
diff --git a/src/bin/pg_verifybackup/t/010_client_untar.pl b/src/bin/pg_verifybackup/t/010_client_untar.pl
index b62faeb5acf..5b0e76ee69d 100644
--- a/src/bin/pg_verifybackup/t/010_client_untar.pl
+++ b/src/bin/pg_verifybackup/t/010_client_untar.pl
@@ -32,7 +32,6 @@ print $jf $junk_data;
close $jf;
my $backup_path = $primary->backup_dir . '/client-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -137,13 +136,11 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
- rmtree($extract_path);
rmtree($backup_path);
}
}
--
2.47.1
On Fri, Nov 21, 2025 at 5:14 PM Amul Sul <sulamul@gmail.com> wrote:
On Wed, Nov 19, 2025 at 1:50 PM Jakub Wartak
<jakub.wartak@enterprisedb.com> wrote:On Mon, Nov 17, 2025 at 5:51 AM Amul Sul <sulamul@gmail.com> wrote:
On Thu, Nov 6, 2025 at 2:33 PM Amul Sul <sulamul@gmail.com> wrote:
On Mon, Oct 20, 2025 at 8:05 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Oct 16, 2025 at 7:49 AM Amul Sul <sulamul@gmail.com> wrote:
[....]Kindly have a look at the attached version. Thank you !
Attached is the rebased version against the latest master head (e76defbcf09).
Hi Amul, thanks for working on this. I haven't really looked at the
source code deeply (I trust Robert eyes much more than mine on this
one), just skimmed a little bit:1. As stated earlier, get_tmp_walseg_path() is still vulnerable (it
uses predictable path that could be used by attacker in $TMPDIR)Yeah, I haven't done anything regarding this since I am unsure of what
should be done and what the risks involved are. I am thinking of
taking Robert's opinion on this.
Per offline discussion with Robert and Jakub, I have updated the patch
to use mkdtemp() as suggested, which is already available in the tree
for similar purposes. Thanks !
Regards,
Amul
Attachments:
v8-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchapplication/x-patch; name=v8-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchDownload
From 1713593f78bb7799ef278424b0efa56acef8bfba Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 24 Jun 2025 11:33:20 +0530
Subject: [PATCH v8 1/8] Refactor: pg_waldump: Move some declarations to new
pg_waldump.h
This change prepares for a second source file in this directory to
support reading WAL from tar files. Common structures, declarations,
and functions are being exported through this include file so
they can be used in both files.
---
src/bin/pg_waldump/pg_waldump.c | 11 ++---------
src/bin/pg_waldump/pg_waldump.h | 27 +++++++++++++++++++++++++++
2 files changed, 29 insertions(+), 9 deletions(-)
create mode 100644 src/bin/pg_waldump/pg_waldump.h
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index c6d6ba79e44..5846ee24f46 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,19 +40,11 @@
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};
-typedef struct XLogDumpPrivate
-{
- TimeLineID timeline;
- XLogRecPtr startptr;
- XLogRecPtr endptr;
- bool endptr_reached;
-} XLogDumpPrivate;
-
typedef struct XLogDumpConfig
{
/* display options */
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
new file mode 100644
index 00000000000..9e62b64ead5
--- /dev/null
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -0,0 +1,27 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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"
+
+extern int WalSegSz;
+
+/* Contains the necessary information to drive WAL decoding */
+typedef struct XLogDumpPrivate
+{
+ TimeLineID timeline;
+ XLogRecPtr startptr;
+ XLogRecPtr endptr;
+ bool endptr_reached;
+} XLogDumpPrivate;
+
+#endif /* end of PG_WALDUMP_H */
--
2.47.1
v8-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchapplication/x-patch; name=v8-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchDownload
From c5ce68512a54b7f586b458374f689df406f39ad6 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 26 Jun 2025 11:42:53 +0530
Subject: [PATCH v8 2/8] Refactor: pg_waldump: Separate logic used to calculate
the required read size.
This refactoring prepares the codebase for an upcoming patch that will
support reading WAL from tar files. The logic for calculating the
required read size has been updated to handle both normal WAL files
and WAL files located inside a tar archive.
---
src/bin/pg_waldump/pg_waldump.c | 39 ++++++++++++++++++++++-----------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 5846ee24f46..0dc28ea360c 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -326,6 +326,29 @@ identify_target_directory(char *directory, char *fname)
return NULL; /* not reached */
}
+/* 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 (XLogRecPtrIsValid(private->endptr))
+ {
+ 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,
@@ -383,21 +406,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 (XLogRecPtrIsValid(private->endptr))
- {
- 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))
--
2.47.1
v8-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchapplication/x-patch; name=v8-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchDownload
From e0a2296c0b79bf3e2c62c712829c9aaa0b516334 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 30 Jul 2025 12:43:30 +0530
Subject: [PATCH v8 3/8] Refactor: pg_waldump: Restructure TAP tests.
Restructured some tests to run inside a loop, facilitating their
re-execution for decoding WAL from tar archives.
---
src/bin/pg_waldump/t/001_basic.pl | 123 ++++++++++++++++--------------
1 file changed, 67 insertions(+), 56 deletions(-)
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index f26d75e01cf..1b712e8d74d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -198,28 +198,6 @@ command_like(
],
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 +205,6 @@ command_like(
],
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.
@@ -272,7 +241,6 @@ sub test_pg_waldump
my $result = IPC::Run::run [
'pg_waldump',
- '--path' => $node->data_dir,
'--start' => $start_lsn,
'--end' => $end_lsn,
@opts
@@ -288,38 +256,81 @@ sub test_pg_waldump
my @lines;
-@lines = test_pg_waldump;
-is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
+my @scenario = (
+ {
+ 'path' => $node->data_dir
+ });
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenario)
+{
+ my $path = $scenario->{'path'};
-@lines = test_pg_waldump('--fullpage');
-is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
+ SKIP:
+ {
+ 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');
-@lines = test_pg_waldump('--stats');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+ command_fails_like(
+ [
+ 'pg_waldump', '--quiet',
+ '--path' => $path,
+ '--start' => $start_lsn
+ ],
+ qr/error: error in WAL record at/,
+ 'errors are shown with --quiet');
-@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('--path' => $path);
+ is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
-@lines = test_pg_waldump('--rmgr' => 'Btree');
-is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+ @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ is(@lines, 6, 'limit option observed');
-@lines = test_pg_waldump('--fork' => 'init');
-is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+ @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
-@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('--path' => $path, '--stats');
+ like($lines[0], qr/WAL statistics/, "statistics on stdout");
+ is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
-@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');
+ @lines = test_pg_waldump('--path' => $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' => $path, '--rmgr' => 'Btree');
+ is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+
+ @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+
+ @lines = test_pg_waldump('--path' => $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' => $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');
+ }
+}
done_testing();
--
2.47.1
v8-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchapplication/x-patch; name=v8-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchDownload
From 7686e875c9da4cedafb884dd4cd153d35c96d540 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 5 Nov 2025 15:40:36 +0530
Subject: [PATCH v8 4/8] pg_waldump: Add support for archived WAL decoding.
pg_waldump can now accept the path to a tar archive containing WAL
files and decode them. This feature was added primarily for
pg_verifybackup, which previously disabled WAL parsing for
tar-formatted backups.
Note that this patch requires that the WAL files within the archive be
in sequential order; an error will be reported otherwise. The next
patch is planned to remove this restriction.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/Makefile | 7 +-
src/bin/pg_waldump/archive_waldump.c | 589 +++++++++++++++++++++++++++
src/bin/pg_waldump/meson.build | 4 +-
src/bin/pg_waldump/pg_waldump.c | 215 +++++++---
src/bin/pg_waldump/pg_waldump.h | 36 +-
src/bin/pg_waldump/t/001_basic.pl | 84 +++-
src/tools/pgindent/typedefs.list | 3 +
8 files changed, 870 insertions(+), 76 deletions(-)
create mode 100644 src/bin/pg_waldump/archive_waldump.c
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index ce23add5577..d004bb0f67e 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -141,13 +141,17 @@ PostgreSQL documentation
<term><option>--path=<replaceable>path</replaceable></option></term>
<listitem>
<para>
- Specifies a directory to search for WAL segment files or a
- directory with a <literal>pg_wal</literal> subdirectory that
+ Specifies a tar archive or a directory to search for WAL segment files
+ or a directory with a <literal>pg_wal</literal> subdirectory that
contains such files. The default is to search in the current
directory, the <literal>pg_wal</literal> subdirectory of the
current directory, and the <literal>pg_wal</literal> subdirectory
of <envar>PGDATA</envar>.
</para>
+ <para>
+ If a tar archive is provided, its WAL segment files must be in
+ sequential order; otherwise, an error will be reported.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile
index 4c1ee649501..05ac5763a57 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 \
+ archive_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/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
new file mode 100644
index 00000000000..f991633e58c
--- /dev/null
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -0,0 +1,589 @@
+/*-------------------------------------------------------------------------
+ *
+ * archive_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/archive_waldump.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <unistd.h>
+
+#include "access/xlog_internal.h"
+#include "common/hashfn.h"
+#include "common/logging.h"
+#include "fe_utils/simple_list.h"
+#include "pg_waldump.h"
+
+/*
+ * How many bytes should we try to read from a file at once?
+ */
+#define READ_CHUNK_SIZE (128 * 1024)
+
+/* Structure for storing the WAL segment data from the archive */
+typedef struct ArchivedWALEntry
+{
+ uint32 status; /* hash status */
+ XLogSegNo segno; /* hash key: WAL segment number */
+ TimeLineID timeline; /* timeline of this wal file */
+
+ StringInfoData buf;
+ bool tmpseg_exists; /* spill file exists? */
+
+ int total_read; /* total read of this WAL segment, including
+ * buffered and temporarily written data */
+} ArchivedWALEntry;
+
+#define SH_PREFIX ArchivedWAL
+#define SH_ELEMENT_TYPE ArchivedWALEntry
+#define SH_KEY_TYPE XLogSegNo
+#define SH_KEY segno
+#define SH_HASH_KEY(tb, key) murmurhash64((uint64) key)
+#define SH_EQUAL(tb, a, b) (a == b)
+#define SH_GET_HASH(tb, a) a->hash
+#define SH_SCOPE static inline
+#define SH_RAW_ALLOCATOR pg_malloc0
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
+
+static ArchivedWAL_hash *ArchivedWAL_HTAB = NULL;
+
+typedef struct astreamer_waldump
+{
+ astreamer base;
+ XLogDumpPrivate *privateInfo;
+} astreamer_waldump;
+
+static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
+static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
+ XLogDumpPrivate *privateInfo);
+
+static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
+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_wal_file(astreamer_waldump *mystreamer,
+ astreamer_member *member,
+ XLogSegNo *curSegNo,
+ TimeLineID *curTimeline);
+
+static const astreamer_ops astreamer_waldump_ops = {
+ .content = astreamer_waldump_content,
+ .finalize = astreamer_waldump_finalize,
+ .free = astreamer_waldump_free
+};
+
+/*
+ * Returns true if the given file is a tar archive and outputs its compression
+ * algorithm.
+ */
+bool
+is_archive_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;
+}
+
+/*
+ * Initializes the tar archive reader to read WAL files from the archive,
+ * creates a hash table to store them, performs quick existence checks for WAL
+ * entries in the archive and retrieves the WAL segment size, and sets up
+ * filtering criteria for relevant entries.
+ */
+void
+init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
+ pg_compress_algorithm compression)
+{
+ int fd;
+ astreamer *streamer;
+ ArchivedWALEntry *entry = NULL;
+ XLogLongPageHeader longhdr;
+
+ /* Open tar archive and store its file descriptor */
+ fd = open_file_in_directory(waldir, privateInfo->archive_name);
+
+ if (fd < 0)
+ pg_fatal("could not open file \"%s\"", privateInfo->archive_name);
+
+ privateInfo->archive_fd = fd;
+
+ streamer = astreamer_waldump_new(privateInfo);
+
+ /* 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);
+
+ privateInfo->archive_streamer = streamer;
+
+ /* Hash table storing WAL entries read from the archive */
+ ArchivedWAL_HTAB = ArchivedWAL_create(16, NULL);
+
+ /*
+ * Verify that the archive contains valid WAL files and fetch WAL segment
+ * size
+ */
+ while (entry == NULL || entry->buf.len < XLOG_BLCKSZ)
+ {
+ if (read_archive_file(privateInfo, XLOG_BLCKSZ) == 0)
+ pg_fatal("could not find WAL in \"%s\" archive",
+ privateInfo->archive_name);
+
+ entry = privateInfo->cur_wal;
+ }
+
+ /* Set WalSegSz if WAL data is successfully read */
+ longhdr = (XLogLongPageHeader) entry->buf.data;
+
+ WalSegSz = longhdr->xlp_seg_size;
+
+ if (!IsValidWalSegSize(WalSegSz))
+ {
+ pg_log_error(ngettext("invalid WAL segment size in WAL file from archive \"%s\" (%d byte)",
+ "invalid WAL segment size in WAL file from archive \"%s\" (%d bytes)",
+ WalSegSz),
+ privateInfo->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);
+ }
+
+ /*
+ * With the WAL segment size available, we can now initialize the
+ * dependent start and end segment numbers.
+ */
+ Assert(!XLogRecPtrIsInvalid(privateInfo->startptr));
+ XLByteToSeg(privateInfo->startptr, privateInfo->startSegNo, WalSegSz);
+
+ if (XLogRecPtrIsInvalid(privateInfo->endptr))
+ privateInfo->endSegNo = UINT64_MAX;
+ else
+ XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+}
+
+/*
+ * Release the archive streamer chain and close the archive file.
+ */
+void
+free_archive_reader(XLogDumpPrivate *privateInfo)
+{
+ /*
+ * 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(privateInfo->archive_streamer);
+
+ /* Close the file. */
+ if (close(privateInfo->archive_fd) != 0)
+ pg_log_error("could not close file \"%s\": %m",
+ privateInfo->archive_name);
+}
+
+/*
+ * Copies WAL data from astreamer to readBuff; if unavailable, fetches more
+ * from the tar archive via astreamer.
+ */
+int
+read_archive_wal_page(XLogDumpPrivate *privateInfo, XLogRecPtr targetPagePtr,
+ Size count, char *readBuff)
+{
+ char *p = readBuff;
+ Size nbytes = count;
+ XLogRecPtr recptr = targetPagePtr;
+ XLogSegNo segno;
+ ArchivedWALEntry *entry;
+
+ XLByteToSeg(targetPagePtr, segno, WalSegSz);
+ entry = get_archive_wal_entry(segno, privateInfo);
+
+ while (nbytes > 0)
+ {
+ char *buf = entry->buf.data;
+ int len = entry->buf.len;
+
+ /* WAL record range that the buffer contains */
+ XLogRecPtr endPtr;
+ XLogRecPtr startPtr;
+
+ XLogSegNoOffsetToRecPtr(entry->segno, entry->total_read,
+ WalSegSz, endPtr);
+ startPtr = endPtr - len;
+
+ /*
+ * pg_waldump may request to re-read the currently active page, but
+ * never a page older than the current one. Therefore, any fully
+ * consumed WAL data preceding the current page can be safely
+ * discarded.
+ */
+ if (recptr >= endPtr)
+ {
+ /* Discard the buffered data */
+ resetStringInfo(&entry->buf);
+ len = 0;
+
+ /*
+ * Push back the partial page data for the current page to the
+ * buffer, ensuring it remains available for re-reading if
+ * requested.
+ */
+ if (p > readBuff)
+ {
+ Assert((count - nbytes) > 0);
+ appendBinaryStringInfo(&entry->buf, readBuff, count - nbytes);
+ }
+ }
+
+ if (len > 0 && recptr > startPtr)
+ {
+ int skipBytes = 0;
+
+ /*
+ * The required offset is not at the start of the 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 the reading page is in the buffer */
+ Assert(recptr >= startPtr && recptr < endPtr);
+
+ memcpy(p, buf, readBytes);
+
+ /* Update state for read */
+ nbytes -= readBytes;
+ p += readBytes;
+ recptr += readBytes;
+ }
+ else
+ {
+ /*
+ * Fetch more data; raise an error if it's not the current segment
+ * being read by the archive streamer or if reading of the
+ * archived file has finished.
+ */
+ if (privateInfo->cur_wal != entry ||
+ read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ {
+ char fname[MAXFNAMELEN];
+
+ XLogFileName(fname, privateInfo->timeline, entry->segno,
+ WalSegSz);
+ pg_fatal("could not read file \"%s\" from archive \"%s\": read %lld of %lld",
+ fname, privateInfo->archive_name,
+ (long long int) count - nbytes,
+ (long long int) nbytes);
+ }
+ }
+ }
+
+ /*
+ * Should have either have successfully read all the requested bytes or
+ * reported a failure before this point.
+ */
+ Assert(nbytes == 0);
+
+ /*
+ * NB: We return the fixed value provided as input. Although we could
+ * return a boolean since we either successfully read the WAL page or
+ * raise an error, but the caller expects this value to be returned. The
+ * routine that reads WAL pages from the physical WAL file follows the
+ * same convention.
+ */
+ return count;
+}
+
+/*
+ * Reads the archive file and passes it to the archive streamer for
+ * decompression.
+ */
+static int
+read_archive_file(XLogDumpPrivate *privateInfo, Size count)
+{
+ int rc;
+ char *buffer;
+
+ buffer = pg_malloc(READ_CHUNK_SIZE * sizeof(uint8));
+
+ rc = read(privateInfo->archive_fd, buffer, count);
+ if (rc < 0)
+ pg_fatal("could not read file \"%s\": %m",
+ privateInfo->archive_name);
+
+ /*
+ * Decompress (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;
+}
+
+/*
+ * Returns the archived WAL entry from the hash table if it exists. Otherwise,
+ * it invokes the routine to read the archived file and retrieve the entry if
+ * it is not already in hash table.
+ */
+static ArchivedWALEntry *
+get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
+{
+ ArchivedWALEntry *entry = NULL;
+ char fname[MAXFNAMELEN];
+
+ /* Search hash table */
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry != NULL)
+ return entry;
+
+ /* Needed WAL yet to be decoded from archive, do the same */
+ while (1)
+ {
+ entry = privateInfo->cur_wal;
+
+ /* Fetch more data */
+ if (entry == NULL || entry->buf.len == 0)
+ {
+ if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ break; /* archive file ended */
+ }
+
+ /*
+ * Either, here for the first time, or the archived streamer is
+ * reading a non-WAL file or an irrelevant WAL file.
+ */
+ if (entry == NULL)
+ continue;
+
+ /* Found the required entry */
+ if (entry->segno == segno)
+ return entry;
+
+ /*
+ * Ignore if the timeline is different or the current segment is not
+ * the desired one.
+ */
+ if (privateInfo->timeline != entry->timeline ||
+ privateInfo->startSegNo > entry->segno ||
+ privateInfo->endSegNo < entry->segno)
+ {
+ privateInfo->cur_wal = NULL;
+ continue;
+ }
+
+ /* WAL segments must be archived in order */
+ pg_log_error("WAL files are not archived in sequential order");
+ pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
+ segno, entry->segno);
+ exit(1);
+ }
+
+ /* Requested WAL segment not found */
+ XLogFileName(fname, privateInfo->timeline, segno, WalSegSz);
+ pg_fatal("could not find file \"%s\" in archive", fname);
+}
+
+/*
+ * Create an astreamer that can read WAL from tar file.
+ */
+static astreamer *
+astreamer_waldump_new(XLogDumpPrivate *privateInfo)
+{
+ astreamer_waldump *streamer;
+
+ streamer = palloc0(sizeof(astreamer_waldump));
+ *((const astreamer_ops **) &streamer->base.bbs_ops) =
+ &astreamer_waldump_ops;
+
+ streamer->privateInfo = privateInfo;
+
+ return &streamer->base;
+}
+
+/*
+ * Main entry point of the archive streamer for reading WAL data from a tar
+ * file. If a member is identified as a valid WAL file, a hash entry is created
+ * for it, and its contents are copied into that entry's buffer, making them
+ * accessible to the decoding routine.
+ */
+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:
+ {
+ XLogSegNo segno;
+ TimeLineID timeline;
+ ArchivedWALEntry *entry;
+ bool found;
+
+ pg_log_debug("reading \"%s\"", member->pathname);
+
+ if (!member_is_wal_file(mystreamer, member,
+ &segno, &timeline))
+ break;
+
+ entry = ArchivedWAL_insert(ArchivedWAL_HTAB, segno, &found);
+
+ /*
+ * Shouldn't happen, but if it does, simply ignore the
+ * duplicate WAL file.
+ */
+ if (found)
+ {
+ pg_log_warning("ignoring duplicate WAL file found in archive: \"%s\"",
+ member->pathname);
+ break;
+ }
+
+ initStringInfo(&entry->buf);
+ entry->timeline = timeline;
+ entry->total_read = 0;
+
+ privateInfo->cur_wal = entry;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_CONTENTS:
+ if (privateInfo->cur_wal)
+ {
+ appendBinaryStringInfo(&privateInfo->cur_wal->buf, data, len);
+ privateInfo->cur_wal->total_read += len;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_TRAILER:
+ privateInfo->cur_wal = NULL;
+ 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)
+{
+ Assert(streamer->bbs_next == NULL);
+ pfree(streamer);
+}
+
+/*
+ * Returns true if the archive member name matches the WAL naming format. If
+ * successful, it also outputs the WAL segment number, and timeline.
+ */
+static bool
+member_is_wal_file(astreamer_waldump *mystreamer, astreamer_member *member,
+ XLogSegNo *curSegNo, TimeLineID *curTimeline)
+{
+ 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;
+
+ /*
+ * XXX: On some systems (e.g., OpenBSD), the tar utility includes
+ * PaxHeaders when creating an archive. These are special entries that
+ * store extended metadata for the file entry immediately following them,
+ * and they share the exact same name as that file.
+ */
+ if (strstr(member->pathname, "PaxHeaders."))
+ return false;
+
+ /* Parse position from file */
+ XLogFromFileName(fname, &timeline, &segNo, WalSegSz);
+
+ *curSegNo = segNo;
+ *curTimeline = timeline;
+
+ return true;
+}
diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build
index 937e0d68841..da00746587c 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',
+ 'archive_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 0dc28ea360c..02ad141e44a 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -177,7 +177,7 @@ split_path(const char *path, char **dir, char **fname)
*
* return a read only fd
*/
-static int
+int
open_file_in_directory(const char *directory, const char *fname)
{
int fd = -1;
@@ -436,6 +436,44 @@ 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);
+
+ if (private->endptr_reached)
+ return -1;
+
+ /* Read the WAL page from the archive streamer */
+ return read_archive_wal_page(private, targetPagePtr, count, readBuff);
+}
+
/*
* Boolean to return whether the given WAL record matches a specific relation
* and optionally block.
@@ -773,8 +811,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"
@@ -806,7 +844,10 @@ main(int argc, char **argv)
XLogRecord *record;
XLogRecPtr first_record;
char *waldir = NULL;
+ char *walpath = NULL;
char *errormsg;
+ bool is_archive = false;
+ pg_compress_algorithm compression;
static struct option long_options[] = {
{"bkp-details", no_argument, NULL, 'b'},
@@ -938,7 +979,7 @@ main(int argc, char **argv)
}
break;
case 'p':
- waldir = pg_strdup(optarg);
+ walpath = pg_strdup(optarg);
break;
case 'q':
config.quiet = true;
@@ -1102,10 +1143,20 @@ main(int argc, char **argv)
goto bad_argument;
}
- if (waldir != NULL)
+ if (walpath != NULL)
{
+ /* validate path points to tar archive */
+ if (is_archive_file(walpath, &compression))
+ {
+ char *fname = NULL;
+
+ split_path(walpath, &waldir, &fname);
+
+ private.archive_name = fname;
+ is_archive = 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;
@@ -1123,6 +1174,17 @@ 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_archive)
+ {
+ 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)
@@ -1133,69 +1195,77 @@ main(int argc, char **argv)
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 (!XLogRecPtrIsValid(private.startptr))
- XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr);
- else if (!XLByteInSeg(private.startptr, segno, WalSegSz))
+ if (fname != NULL && is_archive_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_archive = true;
}
-
- /* no second file specified, set end position */
- if (!(optind + 1 < argc) && !XLogRecPtrIsValid(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(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, &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 (!XLogRecPtrIsValid(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 (!XLogRecPtrIsValid(private.endptr))
- XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz,
- private.endptr);
+ /* no second file specified, set end position */
+ if (!(optind + 1 < argc) && !XLogRecPtrIsValid(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 (!XLogRecPtrIsValid(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_archive)
+ waldir = identify_target_directory(walpath, NULL);
/* we don't know what to print */
if (!XLogRecPtrIsValid(private.startptr))
@@ -1207,12 +1277,36 @@ 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_archive)
+ {
+ /*
+ * A NULL WAL directory indicates that the archive file is located
+ * in the current working directory of the pg_waldump execution
+ */
+ waldir = waldir ? pg_strdup(waldir) : pg_strdup(".");
+
+ /* Set up for reading tar file */
+ init_archive_reader(&private, waldir, compression);
+
+ /* 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");
@@ -1321,6 +1415,9 @@ main(int argc, char **argv)
XLogReaderFree(xlogreader_state);
+ if (is_archive)
+ free_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
index 9e62b64ead5..54758c3548a 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -12,9 +12,13 @@
#define PG_WALDUMP_H
#include "access/xlogdefs.h"
+#include "fe_utils/astreamer.h"
extern int WalSegSz;
+/* Forward declaration */
+struct ArchivedWALEntry;
+
/* Contains the necessary information to drive WAL decoding */
typedef struct XLogDumpPrivate
{
@@ -22,6 +26,36 @@ typedef struct XLogDumpPrivate
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;
+
+ /* What the archive streamer is currently reading */
+ struct ArchivedWALEntry *cur_wal;
+
+ /*
+ * Although these values can be easily derived from startptr and endptr,
+ * doing so repeatedly for each archived member would be inefficient, as
+ * it would involve recalculating and filtering out irrelevant WAL
+ * segments.
+ */
+ XLogSegNo startSegNo;
+ XLogSegNo endSegNo;
} XLogDumpPrivate;
-#endif /* end of PG_WALDUMP_H */
+extern int open_file_in_directory(const char *directory, const char *fname);
+
+extern bool is_archive_file(const char *fname,
+ pg_compress_algorithm *compression);
+extern void init_archive_reader(XLogDumpPrivate *privateInfo,
+ const char *waldir,
+ pg_compress_algorithm compression);
+extern void free_archive_reader(XLogDumpPrivate *privateInfo);
+extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
+ XLogRecPtr targetPagePtr,
+ Size count, char *readBuff);
+
+#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 1b712e8d74d..443126a9ce6 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -3,10 +3,13 @@
use strict;
use warnings FATAL => 'all';
+use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $tar = $ENV{TAR};
+
program_help_ok('pg_waldump');
program_version_ok('pg_waldump');
program_options_handling_ok('pg_waldump');
@@ -235,7 +238,7 @@ command_like(
sub test_pg_waldump
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my @opts = @_;
+ my ($path, @opts) = @_;
my ($stdout, $stderr);
@@ -243,6 +246,7 @@ sub test_pg_waldump
'pg_waldump',
'--start' => $start_lsn,
'--end' => $end_lsn,
+ '--path' => $path,
@opts
],
'>' => \$stdout,
@@ -254,11 +258,50 @@ sub test_pg_waldump
return @lines;
}
-my @lines;
+# 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 = sort @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
+ '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)
@@ -267,6 +310,19 @@ for my $scenario (@scenario)
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/,
@@ -298,38 +354,42 @@ for my $scenario (@scenario)
qr/error: error in WAL record at/,
'errors are shown with --quiet');
- @lines = test_pg_waldump('--path' => $path);
+ my @lines;
+ @lines = test_pg_waldump($path);
is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
- @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ @lines = test_pg_waldump($path, '--limit' => 6);
is(@lines, 6, 'limit option observed');
- @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ @lines = test_pg_waldump($path, '--fullpage');
is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
- @lines = test_pg_waldump('--path' => $path, '--stats');
+ @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' => $path, '--stats=record');
+ @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' => $path, '--rmgr' => 'Btree');
+ @lines = test_pg_waldump($path, '--rmgr' => 'Btree');
is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
- @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ @lines = test_pg_waldump($path, '--fork' => 'init');
is(grep(!/fork init/, @lines), 0, 'only init fork lines');
- @lines = test_pg_waldump('--path' => $path,
+ @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' => $path,
+ @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'};
}
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 57a8f0366a5..981cdb69175 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -139,6 +139,8 @@ ArchiveOpts
ArchiveShutdownCB
ArchiveStartupCB
ArchiveStreamState
+ArchivedWALEntry
+ArchivedWAL_hash
ArchiverOutput
ArchiverStage
ArrayAnalyzeExtraData
@@ -3466,6 +3468,7 @@ astreamer_recovery_injector
astreamer_tar_archiver
astreamer_tar_parser
astreamer_verify
+astreamer_waldump
astreamer_zstd_frame
auth_password_hook_typ
autovac_table
--
2.47.1
v8-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchapplication/x-patch; name=v8-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchDownload
From 1a41393da777838efb3b095b8ab1cafd1fe92623 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 6 Nov 2025 13:48:33 +0530
Subject: [PATCH v8 5/8] pg_waldump: Remove the restriction on the order of
archived WAL files.
With previous patch, pg_waldump would stop decoding if WAL files were
not in the required sequence. With this patch, decoding will now
continue. Any WAL file that is out of order will be written to a
temporary location, from which it will be read later. Once a temporary
file has been read, it will be removed.
---
src/bin/pg_waldump/archive_waldump.c | 220 +++++++++++++++++++++++++--
src/bin/pg_waldump/pg_waldump.c | 41 ++++-
src/bin/pg_waldump/pg_waldump.h | 4 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
4 files changed, 256 insertions(+), 12 deletions(-)
diff --git a/src/bin/pg_waldump/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
index f991633e58c..d38855e9a10 100644
--- a/src/bin/pg_waldump/archive_waldump.c
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -17,6 +17,7 @@
#include <unistd.h>
#include "access/xlog_internal.h"
+#include "common/file_perm.h"
#include "common/hashfn.h"
#include "common/logging.h"
#include "fe_utils/simple_list.h"
@@ -27,6 +28,9 @@
*/
#define READ_CHUNK_SIZE (128 * 1024)
+/* Temporary exported WAL file directory */
+static char *TmpWalSegDir = NULL;
+
/* Structure for storing the WAL segment data from the archive */
typedef struct ArchivedWALEntry
{
@@ -65,6 +69,11 @@ typedef struct astreamer_waldump
static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
XLogDumpPrivate *privateInfo);
+static void setup_tmpseg_dir(const char *waldir);
+static void cleanup_tmpseg_dir_atexit(void);
+
+static FILE *prepare_tmp_write(XLogSegNo segno);
+static void perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file);
static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
static void astreamer_waldump_content(astreamer *streamer,
@@ -120,10 +129,11 @@ is_archive_file(const char *fname, pg_compress_algorithm *compression)
}
/*
- * Initializes the tar archive reader to read WAL files from the archive,
- * creates a hash table to store them, performs quick existence checks for WAL
- * entries in the archive and retrieves the WAL segment size, and sets up
- * filtering criteria for relevant entries.
+ * Initializes the tar archive reader, creates a hash table for WAL entries,
+ * checks for existing valid WAL segments in the archive file and retrieves the
+ * segment size, and sets up filters for relevant entries. It also configures a
+ * temporary directory for out-of-order WAL data and registers an exit callback
+ * to clean up temporary files.
*/
void
init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
@@ -199,6 +209,13 @@ init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
privateInfo->endSegNo = UINT64_MAX;
else
XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+
+ /*
+ * Setup temporary directory to store WAL segments and set up an exit
+ * callback to remove it upon completion.
+ */
+ setup_tmpseg_dir(waldir);
+ atexit(cleanup_tmpseg_dir_atexit);
}
/*
@@ -374,13 +391,16 @@ read_archive_file(XLogDumpPrivate *privateInfo, Size count)
/*
* Returns the archived WAL entry from the hash table if it exists. Otherwise,
* it invokes the routine to read the archived file and retrieve the entry if
- * it is not already in hash table.
+ * it is not already present in the hash table. If the archive streamer happens
+ * to be reading a WAL from archive file that is not currently needed, that WAL
+ * data is written to a temporary file.
*/
static ArchivedWALEntry *
get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
{
ArchivedWALEntry *entry = NULL;
char fname[MAXFNAMELEN];
+ FILE *write_fp = NULL;
/* Search hash table */
entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
@@ -423,11 +443,32 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
continue;
}
- /* WAL segments must be archived in order */
- pg_log_error("WAL files are not archived in sequential order");
- pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
- segno, entry->segno);
- exit(1);
+ /*
+ * Archive streamer is currently reading a file that isn't the one
+ * asked for, but it's required for a future feature. It should be
+ * written to a temporary location for retrieval when needed.
+ */
+
+ /* Create a temporary file if one does not already exist */
+ if (!entry->tmpseg_exists)
+ {
+ write_fp = prepare_tmp_write(entry->segno);
+ entry->tmpseg_exists = true;
+ }
+
+ /* Flush data from the buffer to the file */
+ perform_tmp_write(entry->segno, &entry->buf, write_fp);
+ resetStringInfo(&entry->buf);
+
+ /*
+ * The change in the current segment entry indicates that the reading
+ * of this file has ended.
+ */
+ if (entry != privateInfo->cur_wal && write_fp != NULL)
+ {
+ fclose(write_fp);
+ write_fp = NULL;
+ }
}
/* Requested WAL segment not found */
@@ -435,6 +476,165 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
pg_fatal("could not find file \"%s\" in archive", fname);
}
+/*
+ * Set up a temporary directory to temporarily store WAL segments.
+ */
+static void
+setup_tmpseg_dir(const char *waldir)
+{
+ char *template;
+
+ /*
+ * Use the directory specified by the TEMDIR environment variable. If it's
+ * not set, use the provided WAL directory to extract WAL file
+ * temporarily.
+ */
+ template = psprintf("%s/waldump_tmp-XXXXXX",
+ getenv("TMPDIR") ? getenv("TMPDIR") : waldir);
+ TmpWalSegDir = mkdtemp(template);
+
+ if (TmpWalSegDir == NULL)
+ pg_fatal("could not create directory \"%s\": %m", template);
+
+ canonicalize_path(TmpWalSegDir);
+
+ pg_log_debug("created directory \"%s\"", TmpWalSegDir);
+}
+
+/*
+ * Removes the temporarily store WAL segments, if any, at exiting.
+ */
+static void
+cleanup_tmpseg_dir_atexit(void)
+{
+ ArchivedWAL_iterator it;
+ ArchivedWALEntry *entry;
+
+ /* Remove temporary segments */
+ ArchivedWAL_start_iterate(ArchivedWAL_HTAB, &it);
+ while ((entry = ArchivedWAL_iterate(ArchivedWAL_HTAB, &it)) != NULL)
+ {
+ if (entry->tmpseg_exists)
+ {
+ remove_tmp_walseg(entry->segno, false);
+ entry->tmpseg_exists = false;
+ }
+ }
+
+ /* Remove temporary directory */
+ if (rmdir(TmpWalSegDir) == 0)
+ pg_log_debug("removed directory \"%s\"", TmpWalSegDir);
+}
+
+/*
+ * Generate the temporary WAL file path.
+ *
+ * Note that the caller is responsible to pfree it.
+ */
+char *
+get_tmp_walseg_path(XLogSegNo segno)
+{
+ char *fpath = (char *) palloc(MAXPGPATH);
+
+ Assert(TmpWalSegDir);
+
+ snprintf(fpath, MAXPGPATH, "%s/%08X%08X",
+ TmpWalSegDir,
+ (uint32) (segno / XLogSegmentsPerXLogId(WalSegSz)),
+ (uint32) (segno % XLogSegmentsPerXLogId(WalSegSz)));
+
+ return fpath;
+}
+
+/*
+ * Routine to check whether a temporary file exists for the corresponding WAL
+ * segment number.
+ */
+bool
+tmp_walseg_exists(XLogSegNo segno)
+{
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry == NULL)
+ return false;
+
+ return entry->tmpseg_exists;
+}
+
+/*
+ * Create an empty placeholder file and return its handle.
+ */
+static FILE *
+prepare_tmp_write(XLogSegNo segno)
+{
+ FILE *file;
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(segno);
+
+ /* 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_debug("temporarily exporting file \"%s\"", fpath);
+ pfree(fpath);
+
+ return file;
+}
+
+/*
+ * Write buffer data to the given file handle.
+ */
+static void
+perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file)
+{
+ Assert(file);
+
+ errno = 0;
+ if (buf->len > 0 && fwrite(buf->data, buf->len, 1, file) != 1)
+ {
+ /*
+ * 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_walseg_path(segno));
+ }
+}
+
+/*
+ * Remove temporary file
+ */
+void
+remove_tmp_walseg(XLogSegNo segno, bool update_entry)
+{
+ char *fpath = get_tmp_walseg_path(segno);
+
+ if (unlink(fpath) == 0)
+ pg_log_debug("removed file \"%s\"", fpath);
+ pfree(fpath);
+
+ /* Update entry if requested */
+ if (update_entry)
+ {
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+ Assert(entry != NULL);
+ entry->tmpseg_exists = false;
+ }
+}
+
/*
* Create an astreamer that can read WAL from tar file.
*/
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 02ad141e44a..4c5974a6ae1 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -466,11 +466,50 @@ TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
{
XLogDumpPrivate *private = state->private_data;
int count = required_read_len(private, targetPagePtr, reqLen);
+ XLogSegNo nextSegNo;
if (private->endptr_reached)
return -1;
- /* Read the WAL page from the archive streamer */
+ /*
+ * 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))
+ {
+ 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. */
+ remove_tmp_walseg(nextSegNo, true);
+ }
+
+ 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
+ */
+ if (tmp_walseg_exists(nextSegNo))
+ {
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(nextSegNo);
+ state->seg.ws_file = open(fpath, O_RDONLY | PG_BINARY, 0);
+ pfree(fpath);
+ }
+ }
+
+ /* Continue reading from the open WAL segment, if any */
+ if (state->seg.ws_file >= 0)
+ return WALDumpReadPage(state, targetPagePtr, count, targetPtr,
+ readBuff);
+
+ /* Otherwise, read the WAL page from the archive streamer */
return read_archive_wal_page(private, targetPagePtr, count, readBuff);
}
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
index 54758c3548a..5c1fb1e080a 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -58,4 +58,8 @@ extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
XLogRecPtr targetPagePtr,
Size count, char *readBuff);
+extern char *get_tmp_walseg_path(XLogSegNo segno);
+extern bool tmp_walseg_exists(XLogSegNo segno);
+extern void remove_tmp_walseg(XLogSegNo segno, bool update_entry);
+
#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 443126a9ce6..d5fa1f6d28d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -7,6 +7,7 @@ use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+use List::Util qw(shuffle);
my $tar = $ENV{TAR};
@@ -272,7 +273,7 @@ sub generate_archive
}
closedir $dh;
- @files = sort @files;
+ @files = shuffle @files;
# move into the WAL directory before archiving files
my $cwd = getcwd;
--
2.47.1
v8-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchapplication/x-patch; name=v8-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchDownload
From 5cc37e03ae3b6cf3d01b681bafdc0cc3cf136c27 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 14:47:43 +0530
Subject: [PATCH v8 6/8] pg_verifybackup: Delay default WAL directory
preparation.
We are not sure whether to parse WAL from a directory or an archive
until the backup format is known. Therefore, we delay preparing the
default WAL directory until the point of parsing. This delay is
harmless, as the WAL directory is not used elsewhere.
---
src/bin/pg_verifybackup/pg_verifybackup.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 8d5befa947f..a502e795b2e 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -285,10 +285,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
@@ -368,6 +364,10 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
+ /* 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 parse the required ranges of WAL records, unless we were told
* not to do so.
--
2.47.1
v8-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchapplication/x-patch; name=v8-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchDownload
From cfb5b951450b65f32b7a2357630c184af36bdba7 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 24 Jul 2025 16:37:43 +0530
Subject: [PATCH v8 7/8] pg_verifybackup: Rename the wal-directory switch to
wal-path
With previous patches to pg_waldump can now decode WAL directly from
tar files. This means you'll be able to specify a tar archive path
instead of a traditional WAL directory.
To keep things consistent and more versatile, we should also
generalize the input switch for pg_verifybackup. It should accept
either a directory or a tar file path that contains WALs. This change
will also aligning it with the existing manifest-path switch naming.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 2 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 22 +++++++++++-----------
src/bin/pg_verifybackup/po/de.po | 4 ++--
src/bin/pg_verifybackup/po/el.po | 4 ++--
src/bin/pg_verifybackup/po/es.po | 4 ++--
src/bin/pg_verifybackup/po/fr.po | 4 ++--
src/bin/pg_verifybackup/po/it.po | 4 ++--
src/bin/pg_verifybackup/po/ja.po | 4 ++--
src/bin/pg_verifybackup/po/ka.po | 4 ++--
src/bin/pg_verifybackup/po/ko.po | 4 ++--
src/bin/pg_verifybackup/po/ru.po | 4 ++--
src/bin/pg_verifybackup/po/sv.po | 4 ++--
src/bin/pg_verifybackup/po/uk.po | 4 ++--
src/bin/pg_verifybackup/po/zh_CN.po | 4 ++--
src/bin/pg_verifybackup/po/zh_TW.po | 4 ++--
src/bin/pg_verifybackup/t/007_wal.pl | 4 ++--
16 files changed, 40 insertions(+), 40 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index 61c12975e4a..e9b8bfd51b1 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -261,7 +261,7 @@ PostgreSQL documentation
<varlistentry>
<term><option>-w <replaceable class="parameter">path</replaceable></option></term>
- <term><option>--wal-directory=<replaceable class="parameter">path</replaceable></option></term>
+ <term><option>--wal-path=<replaceable class="parameter">path</replaceable></option></term>
<listitem>
<para>
Try to parse WAL files stored in the specified directory, rather than
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index a502e795b2e..9fcd6be004e 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -93,7 +93,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 +126,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 +135,7 @@ 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 *pg_waldump_path = NULL;
DIR *dir;
@@ -221,8 +221,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 */
@@ -365,15 +365,15 @@ main(int argc, char **argv)
verify_backup_checksums(&context);
/* By default, look for the WAL in the backup directory, too. */
- if (wal_directory == NULL)
- wal_directory = psprintf("%s/pg_wal", context.backup_directory);
+ if (wal_path == NULL)
+ wal_path = psprintf("%s/pg_wal", context.backup_directory);
/*
* 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
@@ -1198,7 +1198,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 +1208,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 +1376,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 a9e24931100..9b5cd5898cf 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 3e3f20c67c5..81442f51c17 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 0cb958f3448..7f729fa35ba 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 da8c72f6427..09937966fa7 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 317b0b71e7f..4da68d0074e 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 c910fb236cc..a948959b54f 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 982751984c7..ef2799316a8 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 acdc3da5e02..eaf91ef1e98 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 64005feedfd..7fb0e5ab1f6 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 17240feeb5c..97125838e8c 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 034b9764232..63f8041ab38 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 b7d97c8976d..fb6fcae8b82 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 c1b710b0a36..568f972b0bb 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/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl
index babc4f0a86b..b07f80719b0 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', '--no-parse-wal', $backup_path ],
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: $!";
--
2.47.1
v8-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchapplication/x-patch; name=v8-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchDownload
From d0028b96f02c6f15231f4788b88abd9ff46b17f1 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 17 Jul 2025 16:39:36 +0530
Subject: [PATCH v8 8/8] pg_verifybackup: enabled WAL parsing for tar-format
backup
Now that pg_waldump supports decoding from tar archives, we should
leverage this functionality to remove the previous restriction on WAL
parsing for tar-backed formats.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 5 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 66 +++++++++++++------
src/bin/pg_verifybackup/t/002_algorithm.pl | 4 --
src/bin/pg_verifybackup/t/003_corruption.pl | 4 +-
src/bin/pg_verifybackup/t/008_untar.pl | 5 +-
src/bin/pg_verifybackup/t/010_client_untar.pl | 5 +-
6 files changed, 50 insertions(+), 39 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index e9b8bfd51b1..16b50b5a4df 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -36,10 +36,7 @@ PostgreSQL documentation
<literal>backup_manifest</literal> 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 <application>pg_basebackup</application>. However, at present,
- <literal>WAL</literal> verification is supported only for plain-format
- backups. Therefore, if the backup is stored in tar-format, the
- <literal>-n, --no-parse-wal</literal> option should be used.
+ supported by <application>pg_basebackup</application>.
</para>
<para>
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 9fcd6be004e..6915fc7f28e 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);
@@ -136,6 +140,8 @@ main(int argc, char **argv)
bool no_parse_wal = false;
bool quiet = false;
char *wal_path = NULL;
+ char *base_archive_path = NULL;
+ char *wal_archive_path = NULL;
char *pg_waldump_path = NULL;
DIR *dir;
@@ -327,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'.
@@ -346,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.
@@ -364,9 +359,28 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
- /* By default, look for the WAL in the backup directory, too. */
+ /*
+ * 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)
- wal_path = psprintf("%s/pg_wal", context.backup_directory);
+ {
+ 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
@@ -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 <tablespaceoid>.tar file here. */
diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl
index ae16c11bc4d..4f284a9e828 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 1dd60f709cf..f1ebdbb46b4 100644
--- a/src/bin/pg_verifybackup/t/003_corruption.pl
+++ b/src/bin/pg_verifybackup/t/003_corruption.pl
@@ -193,10 +193,8 @@ for my $scenario (@scenario)
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/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl
index bc3d6b352ad..09079a94fee 100644
--- a/src/bin/pg_verifybackup/t/008_untar.pl
+++ b/src/bin/pg_verifybackup/t/008_untar.pl
@@ -47,7 +47,6 @@ my $tsoid = $primary->safe_psql(
SELECT oid FROM pg_tablespace WHERE spcname = 'regress_ts1'));
my $backup_path = $primary->backup_dir . '/server-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -123,14 +122,12 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
rmtree($backup_path);
- rmtree($extract_path);
}
}
diff --git a/src/bin/pg_verifybackup/t/010_client_untar.pl b/src/bin/pg_verifybackup/t/010_client_untar.pl
index b62faeb5acf..5b0e76ee69d 100644
--- a/src/bin/pg_verifybackup/t/010_client_untar.pl
+++ b/src/bin/pg_verifybackup/t/010_client_untar.pl
@@ -32,7 +32,6 @@ print $jf $junk_data;
close $jf;
my $backup_path = $primary->backup_dir . '/client-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -137,13 +136,11 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
- rmtree($extract_path);
rmtree($backup_path);
}
}
--
2.47.1
Hi Amul,
I reviewed the patch and got some comments:
On Nov 25, 2025, at 14:37, Amul Sul <sulamul@gmail.com> wrote:
Regards,
Amul
<v8-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patch><v8-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patch><v8-0003-Refactor-pg_waldump-Restructure-TAP-tests.patch><v8-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patch><v8-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patch><v8-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patch><v8-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patch><v8-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patch>
1 - 0001 - pg_waldump.h
```
+ * pg_waldump.h - decode and display WAL
+ *
+ * Copyright (c) 2013-2025, PostgreSQL Global Development Group
```
This header file is brand new, so copyright year should be only 2025.
2 - 0001 - pg_waldump.c
```
-static int WalSegSz;
+int WalSegSz = DEFAULT_XLOG_SEG_SIZE;
```
0001 claims a refactoring, but if you initialize WalSegSz with DEFAULT_XLOG_SEG_SIZE, then the behavior is changing, this change is no longer a pure refactor.
I would suggest leave WalSegSz uninitiated (compiler will set 0 to it), then no behavior change, so that 0001 stays a self-contained pure refactor.
The other nit thing is that, as “static” is removed, now “WalSegSz” is placed in middle of two static variables, which looks not good. If I were making the code change, I would have moved WalSegSz to after all static variables.
3 - 0002
```
@@ -383,21 +406,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 (XLogRecPtrIsValid(private->endptr))
- {
- 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;
```
This change introduces a logic hole. In old code, it sets private->endptr_reached = true; and return -1. In the code code, count and private->endptr_reached assignments are wrapped into required_read_len(). However, required_read_len() doesn’t check if private->endptr_reached has already been true, so that the logic hole is that, if private->endptr_reached is already true when calling required_read_len(), and required_read_len() returns a positive count, if (private->endptr_reached) will also be satisfied and return -1 from the function.
So, to be safe, we should check “if (count < 0) return -1”.
4 - 0002
```
+/* Returns the size in bytes of the data to be read. */
+static inline int
+required_read_len(XLogDumpPrivate *private, XLogRecPtr targetPagePtr,
+ int reqLen)
+{
```
The function comment is too simple. It doesn’t cover the case where -1 is returned.
5 - 0003
```
+my @scenario = (
+ {
+ 'path' => $node->data_dir
+ });
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenario)
+{
```
"my @scenario” should be "my @scenarios”, so that for line become "for my $scenario (@scenarios)”, a little bit clearer.
6 - 0003
```
+ SKIP:
+ {
```
Why SKIP label is defined here? A SKIP label usually follows a skip statement, for example: in bin/pg_ctl/t/001_start_stop.pl
```
SKIP:
{
skip "unix-style permissions not supported on Windows", 2
if ($windows_os);
ok(-f $logFileName);
ok(check_mode_recursive("$tempdir/data", 0700, 0600));
}
```
7 - 0004 - Makefile
```
$(WIN32RES) \
compat.o \
pg_waldump.o \
+ archive_waldump.o \
rmgrdesc.o \
xlogreader.o \
xlogstats.o
```
Obviously the list was in alphabetical order, so archive_waldump.o should be placed before compat.o.
8 - 0004
```
+/*
+ * 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);
```
Looking the page_read’s spec:
```
/*
* Data input callback
*
* This callback shall read at least reqLen valid bytes of the xlog page
* starting at targetPagePtr, and store them in readBuf. The callback
* shall return the number of bytes read (never more than XLOG_BLCKSZ), or
* -1 on failure. The callback shall sleep, if necessary, to wait for the
* requested bytes to become available. The callback will not be invoked
* again for the same page unless more than the returned number of bytes
* are needed.
*
* targetRecPtr is the position of the WAL record we're reading. Usually
* it is equal to targetPagePtr + reqLen, but sometimes xlogreader needs
* to read and verify the page or segment header, before it reads the
* actual WAL record it's interested in. In that case, targetRecPtr can
* be used to determine which timeline to read the page from.
*
* The callback shall set ->seg.ws_tli to the TLI of the file the page was
* read from.
*/
XLogPageReadCB page_read;
```
It says that page_read must read reqLen bytes, otherwise it should wait for more bytes.
However, TarWALDumpReadPage just calculate how many bytes can read and only read that long, which breaks the protocol. Is it a problem?
9 - 0004
```
+/*
+ * Create an astreamer that can read WAL from tar file.
+ */
+static astreamer *
+astreamer_waldump_new(XLogDumpPrivate *privateInfo)
+{
+ astreamer_waldump *streamer;
+
+ streamer = palloc0(sizeof(astreamer_waldump));
+ *((const astreamer_ops **) &streamer->base.bbs_ops) =
+ &astreamer_waldump_ops;
+
+ streamer->privateInfo = privateInfo;
+
+ return &streamer->base;
+}
```
This function allocates memory for streamer but only returns &streamer->base, so memory of streamer is leaked.
Also, in the function comment, “from tar file” => “from a tar file”.
10 - 0004
```
+ * End-of-stream processing for a astreamer_waldump stream.
```
Nit typo: a => an
11 - 0004
```
+ if (!IsValidWalSegSize(WalSegSz))
+ {
+ pg_log_error(ngettext("invalid WAL segment size in WAL file from archive \"%s\" (%d byte)",
+ "invalid WAL segment size in WAL file from archive \"%s\" (%d bytes)",
+ WalSegSz),
+ privateInfo->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);
+ }
```
Why don’t pg_fatal()?
12 - 0005
```
+ /* Create a temporary file if one does not already exist */
+ if (!entry->tmpseg_exists)
+ {
+ write_fp = prepare_tmp_write(entry->segno);
+ entry->tmpseg_exists = true;
+ }
+
+ /* Flush data from the buffer to the file */
+ perform_tmp_write(entry->segno, &entry->buf, write_fp);
+ resetStringInfo(&entry->buf);
+
+ /*
+ * The change in the current segment entry indicates that the reading
+ * of this file has ended.
+ */
+ if (entry != privateInfo->cur_wal && write_fp != NULL)
+ {
+ fclose(write_fp);
+ write_fp = NULL;
+ }
```
When entry->tmpseg_exists is true, then write_fp will not be initialized, but there should be a check to make sure write_fp is not NULL before perform_tmp_write().
Also, if write_fp != NULL, should we anyway close the file without considering entry != privateInfo->cur_wal? Otherwise write_fp may be left open.
13 - 0005
```
+ * Use the directory specified by the TEMDIR environment variable. If it’s
```
Typo: TEMDIR => TMPDIR
14 - 0005
```
+ * Set up a temporary directory to temporarily store WAL segments.
```
temporary and temporarily are redundant.
No comment for 0007.
15 - 0007
I wonder why we need to manually po files? This is the first time I see a patch including po file changes.
16 - 0008
```
+ {
+ 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);
+ }
```
“wal” should be “WAL”.
In the hint message, there should be a white space between the two sentences.
Again, why not pg_fatal().
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
On Tue, Nov 25, 2025 at 2:21 PM Chao Li <li.evan.chao@gmail.com> wrote:
Hi Amul,
I reviewed the patch and got some comments:
Thanks for the review. Replying inline below.
1 - 0001 - pg_waldump.h ``` + * pg_waldump.h - decode and display WAL + * + * Copyright (c) 2013-2025, PostgreSQL Global Development Group ```This header file is brand new, so copyright year should be only 2025.
Fixed in the attached version.
2 - 0001 - pg_waldump.c ``` -static int WalSegSz; +int WalSegSz = DEFAULT_XLOG_SEG_SIZE; ```0001 claims a refactoring, but if you initialize WalSegSz with DEFAULT_XLOG_SEG_SIZE, then the behavior is changing, this change is no longer a pure refactor.
I would suggest leave WalSegSz uninitiated (compiler will set 0 to it), then no behavior change, so that 0001 stays a self-contained pure refactor.
Agreed.
The other nit thing is that, as “static” is removed, now “WalSegSz” is placed in middle of two static variables, which looks not good. If I were making the code change, I would have moved WalSegSz to after all static variables.
I placed it before the static declaration.
3 - 0002 ``` @@ -383,21 +406,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 (XLogRecPtrIsValid(private->endptr)) - { - 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; ```This change introduces a logic hole. In old code, it sets private->endptr_reached = true; and return -1. In the code code, count and private->endptr_reached assignments are wrapped into required_read_len(). However, required_read_len() doesn’t check if private->endptr_reached has already been true, so that the logic hole is that, if private->endptr_reached is already true when calling required_read_len(), and required_read_len() returns a positive count, if (private->endptr_reached) will also be satisfied and return -1 from the function.
So, to be safe, we should check “if (count < 0) return -1”.
I do not really understand the logical hole where the behaviour is the
same as the previous, but I like the idea of checking endptr_reached.
This is quite unlikely to be true, but it looks like good practice to
check that flag before setting it. Did it that way in the attached
version.
4 - 0002 ``` +/* Returns the size in bytes of the data to be read. */ +static inline int +required_read_len(XLogDumpPrivate *private, XLogRecPtr targetPagePtr, + int reqLen) +{ ```The function comment is too simple. It doesn’t cover the case where -1 is returned.
Okay.
5 - 0003 ``` +my @scenario = ( + { + 'path' => $node->data_dir + });-@lines = test_pg_waldump('--limit' => 6); -is(@lines, 6, 'limit option observed'); +for my $scenario (@scenario) +{ ```"my @scenario” should be "my @scenarios”, so that for line become "for my $scenario (@scenarios)”, a little bit clearer.
Done.
6 - 0003
```
+ SKIP:
+ {
```Why SKIP label is defined here? A SKIP label usually follows a skip statement, for example: in bin/pg_ctl/t/001_start_stop.pl
```
SKIP:
{
skip "unix-style permissions not supported on Windows", 2
if ($windows_os);ok(-f $logFileName);
ok(check_mode_recursive("$tempdir/data", 0700, 0600));
}
```
Yeah, I knew that, but that is needed in the next patch where I wanted
to avoid a large diff when introducing SKIP and the associated
indentation. This patch is not expected to be committed independently,
and I have added a note in the commit message for the same.
7 - 0004 - Makefile
```
$(WIN32RES) \
compat.o \
pg_waldump.o \
+ archive_waldump.o \
rmgrdesc.o \
xlogreader.o \
xlogstats.o
```Obviously the list was in alphabetical order, so archive_waldump.o should be placed before compat.o.
Done.
8 - 0004 ``` +/* + * 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); ```Looking the page_read’s spec:
```
/*
* Data input callback
*
* This callback shall read at least reqLen valid bytes of the xlog page
* starting at targetPagePtr, and store them in readBuf. The callback
* shall return the number of bytes read (never more than XLOG_BLCKSZ), or
* -1 on failure. The callback shall sleep, if necessary, to wait for the
* requested bytes to become available. The callback will not be invoked
* again for the same page unless more than the returned number of bytes
* are needed.
*
* targetRecPtr is the position of the WAL record we're reading. Usually
* it is equal to targetPagePtr + reqLen, but sometimes xlogreader needs
* to read and verify the page or segment header, before it reads the
* actual WAL record it's interested in. In that case, targetRecPtr can
* be used to determine which timeline to read the page from.
*
* The callback shall set ->seg.ws_tli to the TLI of the file the page was
* read from.
*/
XLogPageReadCB page_read;
```It says that page_read must read reqLen bytes, otherwise it should wait for more bytes.
However,
just calculate how many bytes can read and only read that long, which
breaks the protocol. Is it a problem?
The behaviour is the same as the routine used to read the bare WAL
file. I don't think there will be any problem for the pg_waldump.
9 - 0004 ``` +/* + * Create an astreamer that can read WAL from tar file. + */ +static astreamer * +astreamer_waldump_new(XLogDumpPrivate *privateInfo) +{ + astreamer_waldump *streamer; + + streamer = palloc0(sizeof(astreamer_waldump)); + *((const astreamer_ops **) &streamer->base.bbs_ops) = + &astreamer_waldump_ops; + + streamer->privateInfo = privateInfo; + + return &streamer->base; +} ```This function allocates memory for streamer but only returns &streamer->base, so memory of streamer is leaked.
May I know why you think there would be a memory leak? I believe the
address of the structure is the same as the address of its first
member, base. I am returning base because the goal is to return a
generic astreamer type, which is the standard approach used in other
archive streamer code.
Also, in the function comment, “from tar file” => “from a tar file”.
10 - 0004
```
+ * End-of-stream processing for a astreamer_waldump stream.
```Nit typo: a => an
Done.
11 - 0004 ``` + if (!IsValidWalSegSize(WalSegSz)) + { + pg_log_error(ngettext("invalid WAL segment size in WAL file from archive \"%s\" (%d byte)", + "invalid WAL segment size in WAL file from archive \"%s\" (%d bytes)", + WalSegSz), + privateInfo->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); + } ```Why don’t pg_fatal()?
This is how we do when we need to emit error details as well.
12 - 0005 ``` + /* Create a temporary file if one does not already exist */ + if (!entry->tmpseg_exists) + { + write_fp = prepare_tmp_write(entry->segno); + entry->tmpseg_exists = true; + } + + /* Flush data from the buffer to the file */ + perform_tmp_write(entry->segno, &entry->buf, write_fp); + resetStringInfo(&entry->buf); + + /* + * The change in the current segment entry indicates that the reading + * of this file has ended. + */ + if (entry != privateInfo->cur_wal && write_fp != NULL) + { + fclose(write_fp); + write_fp = NULL; + } ```When entry->tmpseg_exists is true, then write_fp will not be initialized, but there should be a check to make sure write_fp is not NULL before perform_tmp_write().
perform_tmp_write() has assert for the same.
Also, if write_fp != NULL, should we anyway close the file without considering entry != privateInfo->cur_wal? Otherwise write_fp may be left open.
We read the WAL from the tar file in chunks, and those same chunks are
written to the temporary file within the loop. If we close the
temporary file now, we will have to open it again later for the next
chunk write. Could you elaborate on a scenario where you believe this
file might be left open unintentionally?
13 - 0005
```
+ * Use the directory specified by the TEMDIR environment variable. If it’s
```Typo: TEMDIR => TMPDIR
Done.
14 - 0005
```
+ * Set up a temporary directory to temporarily store WAL segments.
```temporary and temporarily are redundant.
I believe that is grammatically correct and clear.
No comment for 0007.
15 - 0007
I wonder why we need to manually po files? This is the first time I see a patch including po file changes.
Okay, I included that initially to ensure the PO file update wasn't
overlooked during commit. I have removed it to minimize the diff and
added the note in the patch commit message.
16 - 0008 ``` + { + 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); + } ```“wal” should be “WAL”.
In the hint message, there should be a white space between the two sentences.
Done.
Thanks again for your review comments; they are quite helpful. Kindly
take a look at the attached version.
Regards,
Amul
Attachments:
v9-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchapplication/x-patch; name=v9-0001-Refactor-pg_waldump-Move-some-declarations-to-new.patchDownload
From 38322b7e5062f7c45f47e09655a1c964e9eeb68b Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 24 Jun 2025 11:33:20 +0530
Subject: [PATCH v9 1/8] Refactor: pg_waldump: Move some declarations to new
pg_waldump.h
This change prepares for a second source file in this directory to
support reading WAL from tar files. Common structures, declarations,
and functions are being exported through this include file so
they can be used in both files.
---
src/bin/pg_waldump/pg_waldump.c | 12 +++---------
src/bin/pg_waldump/pg_waldump.h | 27 +++++++++++++++++++++++++++
2 files changed, 30 insertions(+), 9 deletions(-)
create mode 100644 src/bin/pg_waldump/pg_waldump.h
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index c6d6ba79e44..6680280dbbc 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"
@@ -37,21 +38,14 @@
* give a thought about doing the same in pg_walinspect contrib module as well.
*/
+int WalSegSz;
+
static const char *progname;
-static int WalSegSz;
static volatile sig_atomic_t time_to_stop = false;
static const RelFileLocator emptyRelFileLocator = {0, 0, 0};
-typedef struct XLogDumpPrivate
-{
- TimeLineID timeline;
- XLogRecPtr startptr;
- XLogRecPtr endptr;
- bool endptr_reached;
-} XLogDumpPrivate;
-
typedef struct XLogDumpConfig
{
/* display options */
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
new file mode 100644
index 00000000000..926d529f9d6
--- /dev/null
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -0,0 +1,27 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_waldump.h - decode and display WAL
+ *
+ * Copyright (c) 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"
+
+extern int WalSegSz;
+
+/* Contains the necessary information to drive WAL decoding */
+typedef struct XLogDumpPrivate
+{
+ TimeLineID timeline;
+ XLogRecPtr startptr;
+ XLogRecPtr endptr;
+ bool endptr_reached;
+} XLogDumpPrivate;
+
+#endif /* end of PG_WALDUMP_H */
--
2.47.1
v9-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchapplication/x-patch; name=v9-0002-Refactor-pg_waldump-Separate-logic-used-to-calcul.patchDownload
From 01d144fe8af4d4232b4b043c2ef11b07d785b4bd Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 26 Jun 2025 11:42:53 +0530
Subject: [PATCH v9 2/8] Refactor: pg_waldump: Separate logic used to calculate
the required read size.
This refactoring prepares the codebase for an upcoming patch that will
support reading WAL from tar files. The logic for calculating the
required read size has been updated to handle both normal WAL files
and WAL files located inside a tar archive.
---
src/bin/pg_waldump/pg_waldump.c | 46 +++++++++++++++++++++++----------
1 file changed, 33 insertions(+), 13 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 6680280dbbc..2c11e0e5ca1 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -327,6 +327,35 @@ identify_target_directory(char *directory, char *fname)
return NULL; /* not reached */
}
+/*
+ * Returns the size in bytes of the data to be read. Returns -1 if the end
+ * point has already been reached.
+ */
+static inline int
+required_read_len(XLogDumpPrivate *private, XLogRecPtr targetPagePtr,
+ int reqLen)
+{
+ int count = XLOG_BLCKSZ;
+
+ if (unlikely(private->endptr_reached))
+ return -1;
+
+ if (XLogRecPtrIsValid(private->endptr))
+ {
+ 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,
@@ -384,21 +413,12 @@ 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 (XLogRecPtrIsValid(private->endptr))
- {
- 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;
- }
- }
+ /* Bail out if the count to be read is not valid */
+ if (count < 0)
+ return -1;
if (!WALRead(state, readBuff, targetPagePtr, count, private->timeline,
&errinfo))
--
2.47.1
v9-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchapplication/x-patch; name=v9-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchDownload
From 60b4c33b9b95c3d24a8200dae5cbc75bb09daef8 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 25 Nov 2025 16:12:11 +0530
Subject: [PATCH v9 3/8] Refactor: pg_waldump: Restructure TAP tests.
Restructured some tests to run inside a loop, facilitating their
re-execution for decoding WAL from tar archives.
== NOTE ==
This is not intended to be committed separately. It can be merged
with the next patch, which is the main patch implementing this
feature.
---
src/bin/pg_waldump/t/001_basic.pl | 123 ++++++++++++++++--------------
1 file changed, 67 insertions(+), 56 deletions(-)
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index f26d75e01cf..c8fdc7cb4f3 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -198,28 +198,6 @@ command_like(
],
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 +205,6 @@ command_like(
],
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.
@@ -272,7 +241,6 @@ sub test_pg_waldump
my $result = IPC::Run::run [
'pg_waldump',
- '--path' => $node->data_dir,
'--start' => $start_lsn,
'--end' => $end_lsn,
@opts
@@ -288,38 +256,81 @@ sub test_pg_waldump
my @lines;
-@lines = test_pg_waldump;
-is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
+my @scenarios = (
+ {
+ 'path' => $node->data_dir
+ });
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenarios)
+{
+ my $path = $scenario->{'path'};
-@lines = test_pg_waldump('--fullpage');
-is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
+ SKIP:
+ {
+ 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');
-@lines = test_pg_waldump('--stats');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+ command_fails_like(
+ [
+ 'pg_waldump', '--quiet',
+ '--path' => $path,
+ '--start' => $start_lsn
+ ],
+ qr/error: error in WAL record at/,
+ 'errors are shown with --quiet');
-@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('--path' => $path);
+ is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
-@lines = test_pg_waldump('--rmgr' => 'Btree');
-is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+ @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ is(@lines, 6, 'limit option observed');
-@lines = test_pg_waldump('--fork' => 'init');
-is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+ @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
-@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('--path' => $path, '--stats');
+ like($lines[0], qr/WAL statistics/, "statistics on stdout");
+ is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
-@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');
+ @lines = test_pg_waldump('--path' => $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' => $path, '--rmgr' => 'Btree');
+ is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+
+ @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+
+ @lines = test_pg_waldump('--path' => $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' => $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');
+ }
+}
done_testing();
--
2.47.1
v9-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchapplication/x-patch; name=v9-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchDownload
From bd2de61f136dd9111f69874af2782c4b2f6ab314 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 5 Nov 2025 15:40:36 +0530
Subject: [PATCH v9 4/8] pg_waldump: Add support for archived WAL decoding.
pg_waldump can now accept the path to a tar archive containing WAL
files and decode them. This feature was added primarily for
pg_verifybackup, which previously disabled WAL parsing for
tar-formatted backups.
Note that this patch requires that the WAL files within the archive be
in sequential order; an error will be reported otherwise. The next
patch is planned to remove this restriction.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/Makefile | 7 +-
src/bin/pg_waldump/archive_waldump.c | 596 +++++++++++++++++++++++++++
src/bin/pg_waldump/meson.build | 4 +-
src/bin/pg_waldump/pg_waldump.c | 218 +++++++---
src/bin/pg_waldump/pg_waldump.h | 34 ++
src/bin/pg_waldump/t/001_basic.pl | 84 +++-
src/tools/pgindent/typedefs.list | 3 +
8 files changed, 878 insertions(+), 76 deletions(-)
create mode 100644 src/bin/pg_waldump/archive_waldump.c
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index ce23add5577..d004bb0f67e 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -141,13 +141,17 @@ PostgreSQL documentation
<term><option>--path=<replaceable>path</replaceable></option></term>
<listitem>
<para>
- Specifies a directory to search for WAL segment files or a
- directory with a <literal>pg_wal</literal> subdirectory that
+ Specifies a tar archive or a directory to search for WAL segment files
+ or a directory with a <literal>pg_wal</literal> subdirectory that
contains such files. The default is to search in the current
directory, the <literal>pg_wal</literal> subdirectory of the
current directory, and the <literal>pg_wal</literal> subdirectory
of <envar>PGDATA</envar>.
</para>
+ <para>
+ If a tar archive is provided, its WAL segment files must be in
+ sequential order; otherwise, an error will be reported.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile
index 4c1ee649501..aabb87566a2 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
@@ -10,13 +13,15 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
$(RMGRDESCOBJS) \
$(WIN32RES) \
+ archive_waldump.o \
compat.o \
pg_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/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
new file mode 100644
index 00000000000..63141dc2ee2
--- /dev/null
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -0,0 +1,596 @@
+/*-------------------------------------------------------------------------
+ *
+ * archive_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/archive_waldump.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <unistd.h>
+
+#include "access/xlog_internal.h"
+#include "common/hashfn.h"
+#include "common/logging.h"
+#include "fe_utils/simple_list.h"
+#include "pg_waldump.h"
+
+/*
+ * How many bytes should we try to read from a file at once?
+ */
+#define READ_CHUNK_SIZE (128 * 1024)
+
+/* Structure for storing the WAL segment data from the archive */
+typedef struct ArchivedWALEntry
+{
+ uint32 status; /* hash status */
+ XLogSegNo segno; /* hash key: WAL segment number */
+ TimeLineID timeline; /* timeline of this wal file */
+
+ StringInfoData buf;
+ bool tmpseg_exists; /* spill file exists? */
+
+ int total_read; /* total read of this WAL segment, including
+ * buffered and temporarily written data */
+} ArchivedWALEntry;
+
+#define SH_PREFIX ArchivedWAL
+#define SH_ELEMENT_TYPE ArchivedWALEntry
+#define SH_KEY_TYPE XLogSegNo
+#define SH_KEY segno
+#define SH_HASH_KEY(tb, key) murmurhash64((uint64) key)
+#define SH_EQUAL(tb, a, b) (a == b)
+#define SH_GET_HASH(tb, a) a->hash
+#define SH_SCOPE static inline
+#define SH_RAW_ALLOCATOR pg_malloc0
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
+
+static ArchivedWAL_hash *ArchivedWAL_HTAB = NULL;
+
+typedef struct astreamer_waldump
+{
+ astreamer base;
+ XLogDumpPrivate *privateInfo;
+} astreamer_waldump;
+
+static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
+static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
+ XLogDumpPrivate *privateInfo);
+
+static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
+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_wal_file(astreamer_waldump *mystreamer,
+ astreamer_member *member,
+ XLogSegNo *curSegNo,
+ TimeLineID *curTimeline);
+
+static const astreamer_ops astreamer_waldump_ops = {
+ .content = astreamer_waldump_content,
+ .finalize = astreamer_waldump_finalize,
+ .free = astreamer_waldump_free
+};
+
+/*
+ * Returns true if the given file is a tar archive and outputs its compression
+ * algorithm.
+ */
+bool
+is_archive_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;
+}
+
+/*
+ * Initializes the tar archive reader to read WAL files from the archive,
+ * creates a hash table to store them, performs quick existence checks for WAL
+ * entries in the archive and retrieves the WAL segment size, and sets up
+ * filtering criteria for relevant entries.
+ */
+void
+init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
+ pg_compress_algorithm compression)
+{
+ int fd;
+ astreamer *streamer;
+ ArchivedWALEntry *entry = NULL;
+ XLogLongPageHeader longhdr;
+
+ /* Open tar archive and store its file descriptor */
+ fd = open_file_in_directory(waldir, privateInfo->archive_name);
+
+ if (fd < 0)
+ pg_fatal("could not open file \"%s\"", privateInfo->archive_name);
+
+ privateInfo->archive_fd = fd;
+
+ streamer = astreamer_waldump_new(privateInfo);
+
+ /* 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);
+
+ privateInfo->archive_streamer = streamer;
+
+ /* Hash table storing WAL entries read from the archive */
+ ArchivedWAL_HTAB = ArchivedWAL_create(16, NULL);
+
+ /*
+ * Verify that the archive contains valid WAL files and fetch WAL segment
+ * size
+ */
+ while (entry == NULL || entry->buf.len < XLOG_BLCKSZ)
+ {
+ if (read_archive_file(privateInfo, XLOG_BLCKSZ) == 0)
+ pg_fatal("could not find WAL in \"%s\" archive",
+ privateInfo->archive_name);
+
+ entry = privateInfo->cur_wal;
+ }
+
+ /* Set WalSegSz if WAL data is successfully read */
+ longhdr = (XLogLongPageHeader) entry->buf.data;
+
+ WalSegSz = longhdr->xlp_seg_size;
+
+ if (!IsValidWalSegSize(WalSegSz))
+ {
+ pg_log_error(ngettext("invalid WAL segment size in WAL file from archive \"%s\" (%d byte)",
+ "invalid WAL segment size in WAL file from archive \"%s\" (%d bytes)",
+ WalSegSz),
+ privateInfo->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);
+ }
+
+ /*
+ * With the WAL segment size available, we can now initialize the
+ * dependent start and end segment numbers.
+ */
+ Assert(!XLogRecPtrIsInvalid(privateInfo->startptr));
+ XLByteToSeg(privateInfo->startptr, privateInfo->startSegNo, WalSegSz);
+
+ if (XLogRecPtrIsInvalid(privateInfo->endptr))
+ privateInfo->endSegNo = UINT64_MAX;
+ else
+ XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+}
+
+/*
+ * Release the archive streamer chain and close the archive file.
+ */
+void
+free_archive_reader(XLogDumpPrivate *privateInfo)
+{
+ /*
+ * 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(privateInfo->archive_streamer);
+
+ /* Close the file. */
+ if (close(privateInfo->archive_fd) != 0)
+ pg_log_error("could not close file \"%s\": %m",
+ privateInfo->archive_name);
+}
+
+/*
+ * Copies WAL data from astreamer to readBuff; if unavailable, fetches more
+ * from the tar archive via astreamer.
+ */
+int
+read_archive_wal_page(XLogDumpPrivate *privateInfo, XLogRecPtr targetPagePtr,
+ Size count, char *readBuff)
+{
+ char *p = readBuff;
+ Size nbytes = count;
+ XLogRecPtr recptr = targetPagePtr;
+ XLogSegNo segno;
+ ArchivedWALEntry *entry;
+
+ XLByteToSeg(targetPagePtr, segno, WalSegSz);
+ entry = get_archive_wal_entry(segno, privateInfo);
+
+ while (nbytes > 0)
+ {
+ char *buf = entry->buf.data;
+ int len = entry->buf.len;
+
+ /* WAL record range that the buffer contains */
+ XLogRecPtr endPtr;
+ XLogRecPtr startPtr;
+
+ XLogSegNoOffsetToRecPtr(entry->segno, entry->total_read,
+ WalSegSz, endPtr);
+ startPtr = endPtr - len;
+
+ /*
+ * pg_waldump may request to re-read the currently active page, but
+ * never a page older than the current one. Therefore, any fully
+ * consumed WAL data preceding the current page can be safely
+ * discarded.
+ */
+ if (recptr >= endPtr)
+ {
+ /* Discard the buffered data */
+ resetStringInfo(&entry->buf);
+ len = 0;
+
+ /*
+ * Push back the partial page data for the current page to the
+ * buffer, ensuring it remains available for re-reading if
+ * requested.
+ */
+ if (p > readBuff)
+ {
+ Assert((count - nbytes) > 0);
+ appendBinaryStringInfo(&entry->buf, readBuff, count - nbytes);
+ }
+ }
+
+ if (len > 0 && recptr > startPtr)
+ {
+ int skipBytes = 0;
+
+ /*
+ * The required offset is not at the start of the 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 the reading page is in the buffer */
+ Assert(recptr >= startPtr && recptr < endPtr);
+
+ memcpy(p, buf, readBytes);
+
+ /* Update state for read */
+ nbytes -= readBytes;
+ p += readBytes;
+ recptr += readBytes;
+ }
+ else
+ {
+ /*
+ * Fetch more data; raise an error if it's not the current segment
+ * being read by the archive streamer or if reading of the
+ * archived file has finished.
+ */
+ if (privateInfo->cur_wal != entry ||
+ read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ {
+ char fname[MAXFNAMELEN];
+
+ XLogFileName(fname, privateInfo->timeline, entry->segno,
+ WalSegSz);
+ pg_fatal("could not read file \"%s\" from archive \"%s\": read %lld of %lld",
+ fname, privateInfo->archive_name,
+ (long long int) count - nbytes,
+ (long long int) nbytes);
+ }
+ }
+ }
+
+ /*
+ * Should have either have successfully read all the requested bytes or
+ * reported a failure before this point.
+ */
+ Assert(nbytes == 0);
+
+ /*
+ * NB: We return the fixed value provided as input. Although we could
+ * return a boolean since we either successfully read the WAL page or
+ * raise an error, but the caller expects this value to be returned. The
+ * routine that reads WAL pages from the physical WAL file follows the
+ * same convention.
+ */
+ return count;
+}
+
+/*
+ * Reads the archive file and passes it to the archive streamer for
+ * decompression.
+ */
+static int
+read_archive_file(XLogDumpPrivate *privateInfo, Size count)
+{
+ int rc;
+ char *buffer;
+
+ buffer = pg_malloc(READ_CHUNK_SIZE * sizeof(uint8));
+
+ rc = read(privateInfo->archive_fd, buffer, count);
+ if (rc < 0)
+ pg_fatal("could not read file \"%s\": %m",
+ privateInfo->archive_name);
+
+ /*
+ * Decompress (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;
+}
+
+/*
+ * Returns the archived WAL entry from the hash table if it exists. Otherwise,
+ * it invokes the routine to read the archived file and retrieve the entry if
+ * it is not already in hash table.
+ */
+static ArchivedWALEntry *
+get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
+{
+ ArchivedWALEntry *entry = NULL;
+ char fname[MAXFNAMELEN];
+
+ /* Search hash table */
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry != NULL)
+ return entry;
+
+ /* Needed WAL yet to be decoded from archive, do the same */
+ while (1)
+ {
+ entry = privateInfo->cur_wal;
+
+ /* Fetch more data */
+ if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ break; /* archive file ended */
+
+ /*
+ * Either, here for the first time, or the archived streamer is
+ * reading a non-WAL file or an irrelevant WAL file.
+ */
+ if (entry == NULL)
+ continue;
+
+ /* Found the required entry */
+ if (entry->segno == segno)
+ return entry;
+
+ /*
+ * Ignore if the timeline is different or the current segment is not
+ * the desired one.
+ */
+ if (privateInfo->timeline != entry->timeline ||
+ privateInfo->startSegNo > entry->segno ||
+ privateInfo->endSegNo < entry->segno)
+ {
+ privateInfo->cur_wal = NULL;
+ continue;
+ }
+
+ /*
+ * XXX: If the segment being read not the requested one, the data must
+ * be buffered, as we currently lack the mechanism to write it to a
+ * temporary file. This is a known limitation that will be fixed in the
+ * next patch, as the buffer could grow up to the full WAL segment
+ * size.
+ */
+ if (segno > entry->segno)
+ continue;
+
+ /* WAL segments must be archived in order */
+ pg_log_error("WAL files are not archived in sequential order");
+ pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
+ segno, entry->segno);
+ exit(1);
+ }
+
+ /* Requested WAL segment not found */
+ XLogFileName(fname, privateInfo->timeline, segno, WalSegSz);
+ pg_fatal("could not find file \"%s\" in archive", fname);
+}
+
+/*
+ * Create an astreamer that can read WAL from a tar file.
+ */
+static astreamer *
+astreamer_waldump_new(XLogDumpPrivate *privateInfo)
+{
+ astreamer_waldump *streamer;
+
+ streamer = palloc0(sizeof(astreamer_waldump));
+ *((const astreamer_ops **) &streamer->base.bbs_ops) =
+ &astreamer_waldump_ops;
+
+ streamer->privateInfo = privateInfo;
+
+ return &streamer->base;
+}
+
+/*
+ * Main entry point of the archive streamer for reading WAL data from a tar
+ * file. If a member is identified as a valid WAL file, a hash entry is created
+ * for it, and its contents are copied into that entry's buffer, making them
+ * accessible to the decoding routine.
+ */
+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:
+ {
+ XLogSegNo segno;
+ TimeLineID timeline;
+ ArchivedWALEntry *entry;
+ bool found;
+
+ pg_log_debug("reading \"%s\"", member->pathname);
+
+ if (!member_is_wal_file(mystreamer, member,
+ &segno, &timeline))
+ break;
+
+ entry = ArchivedWAL_insert(ArchivedWAL_HTAB, segno, &found);
+
+ /*
+ * Shouldn't happen, but if it does, simply ignore the
+ * duplicate WAL file.
+ */
+ if (found)
+ {
+ pg_log_warning("ignoring duplicate WAL file found in archive: \"%s\"",
+ member->pathname);
+ break;
+ }
+
+ initStringInfo(&entry->buf);
+ entry->timeline = timeline;
+ entry->total_read = 0;
+
+ privateInfo->cur_wal = entry;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_CONTENTS:
+ if (privateInfo->cur_wal)
+ {
+ appendBinaryStringInfo(&privateInfo->cur_wal->buf, data, len);
+ privateInfo->cur_wal->total_read += len;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_TRAILER:
+ privateInfo->cur_wal = NULL;
+ break;
+
+ case ASTREAMER_ARCHIVE_TRAILER:
+ break;
+
+ default:
+ /* Shouldn't happen. */
+ pg_fatal("unexpected state while parsing tar file");
+ }
+}
+
+/*
+ * End-of-stream processing for an 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)
+{
+ Assert(streamer->bbs_next == NULL);
+ pfree(streamer);
+}
+
+/*
+ * Returns true if the archive member name matches the WAL naming format. If
+ * successful, it also outputs the WAL segment number, and timeline.
+ */
+static bool
+member_is_wal_file(astreamer_waldump *mystreamer, astreamer_member *member,
+ XLogSegNo *curSegNo, TimeLineID *curTimeline)
+{
+ 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;
+
+ /*
+ * XXX: On some systems (e.g., OpenBSD), the tar utility includes
+ * PaxHeaders when creating an archive. These are special entries that
+ * store extended metadata for the file entry immediately following them,
+ * and they share the exact same name as that file.
+ */
+ if (strstr(member->pathname, "PaxHeaders."))
+ return false;
+
+ /* Parse position from file */
+ XLogFromFileName(fname, &timeline, &segNo, WalSegSz);
+
+ *curSegNo = segNo;
+ *curTimeline = timeline;
+
+ return true;
+}
diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build
index 937e0d68841..f31e0d1cd86 100644
--- a/src/bin/pg_waldump/meson.build
+++ b/src/bin/pg_waldump/meson.build
@@ -1,6 +1,7 @@
# Copyright (c) 2022-2025, PostgreSQL Global Development Group
pg_waldump_sources = files(
+ 'archive_waldump.c',
'compat.c',
'pg_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 2c11e0e5ca1..1eedf8e01b4 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -38,7 +38,7 @@
* give a thought about doing the same in pg_walinspect contrib module as well.
*/
-int WalSegSz;
+int WalSegSz = DEFAULT_XLOG_SEG_SIZE;
static const char *progname;
@@ -178,7 +178,7 @@ split_path(const char *path, char **dir, char **fname)
*
* return a read only fd
*/
-static int
+int
open_file_in_directory(const char *directory, const char *fname)
{
int fd = -1;
@@ -444,6 +444,45 @@ 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);
+
+ /* Bail out if the count to be read is not valid */
+ if (count < 0)
+ return -1;
+
+ /* Read the WAL page from the archive streamer */
+ return read_archive_wal_page(private, targetPagePtr, count, readBuff);
+}
+
/*
* Boolean to return whether the given WAL record matches a specific relation
* and optionally block.
@@ -781,8 +820,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"
@@ -814,7 +853,10 @@ main(int argc, char **argv)
XLogRecord *record;
XLogRecPtr first_record;
char *waldir = NULL;
+ char *walpath = NULL;
char *errormsg;
+ bool is_archive = false;
+ pg_compress_algorithm compression;
static struct option long_options[] = {
{"bkp-details", no_argument, NULL, 'b'},
@@ -946,7 +988,7 @@ main(int argc, char **argv)
}
break;
case 'p':
- waldir = pg_strdup(optarg);
+ walpath = pg_strdup(optarg);
break;
case 'q':
config.quiet = true;
@@ -1110,10 +1152,20 @@ main(int argc, char **argv)
goto bad_argument;
}
- if (waldir != NULL)
+ if (walpath != NULL)
{
+ /* validate path points to tar archive */
+ if (is_archive_file(walpath, &compression))
+ {
+ char *fname = NULL;
+
+ split_path(walpath, &waldir, &fname);
+
+ private.archive_name = fname;
+ is_archive = 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;
@@ -1131,6 +1183,17 @@ 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_archive)
+ {
+ 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)
@@ -1141,69 +1204,77 @@ main(int argc, char **argv)
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 (!XLogRecPtrIsValid(private.startptr))
- XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr);
- else if (!XLByteInSeg(private.startptr, segno, WalSegSz))
+ if (fname != NULL && is_archive_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_archive = true;
}
-
- /* no second file specified, set end position */
- if (!(optind + 1 < argc) && !XLogRecPtrIsValid(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(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, &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 (!XLogRecPtrIsValid(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 (!XLogRecPtrIsValid(private.endptr))
- XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz,
- private.endptr);
+ /* no second file specified, set end position */
+ if (!(optind + 1 < argc) && !XLogRecPtrIsValid(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 (!XLogRecPtrIsValid(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_archive)
+ waldir = identify_target_directory(walpath, NULL);
/* we don't know what to print */
if (!XLogRecPtrIsValid(private.startptr))
@@ -1215,12 +1286,36 @@ 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_archive)
+ {
+ /*
+ * A NULL WAL directory indicates that the archive file is located in
+ * the current working directory of the pg_waldump execution
+ */
+ waldir = waldir ? pg_strdup(waldir) : pg_strdup(".");
+
+ /* Set up for reading tar file */
+ init_archive_reader(&private, waldir, compression);
+
+ /* 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");
@@ -1329,6 +1424,9 @@ main(int argc, char **argv)
XLogReaderFree(xlogreader_state);
+ if (is_archive)
+ free_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
index 926d529f9d6..ec7a33d40e0 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -12,9 +12,13 @@
#define PG_WALDUMP_H
#include "access/xlogdefs.h"
+#include "fe_utils/astreamer.h"
extern int WalSegSz;
+/* Forward declaration */
+struct ArchivedWALEntry;
+
/* Contains the necessary information to drive WAL decoding */
typedef struct XLogDumpPrivate
{
@@ -22,6 +26,36 @@ typedef struct XLogDumpPrivate
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;
+
+ /* What the archive streamer is currently reading */
+ struct ArchivedWALEntry *cur_wal;
+
+ /*
+ * Although these values can be easily derived from startptr and endptr,
+ * doing so repeatedly for each archived member would be inefficient, as
+ * it would involve recalculating and filtering out irrelevant WAL
+ * segments.
+ */
+ XLogSegNo startSegNo;
+ XLogSegNo endSegNo;
} XLogDumpPrivate;
+extern int open_file_in_directory(const char *directory, const char *fname);
+
+extern bool is_archive_file(const char *fname,
+ pg_compress_algorithm *compression);
+extern void init_archive_reader(XLogDumpPrivate *privateInfo,
+ const char *waldir,
+ pg_compress_algorithm compression);
+extern void free_archive_reader(XLogDumpPrivate *privateInfo);
+extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
+ XLogRecPtr targetPagePtr,
+ Size count, char *readBuff);
+
#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 c8fdc7cb4f3..b12bbc6f95b 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -3,10 +3,13 @@
use strict;
use warnings FATAL => 'all';
+use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $tar = $ENV{TAR};
+
program_help_ok('pg_waldump');
program_version_ok('pg_waldump');
program_options_handling_ok('pg_waldump');
@@ -235,7 +238,7 @@ command_like(
sub test_pg_waldump
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my @opts = @_;
+ my ($path, @opts) = @_;
my ($stdout, $stderr);
@@ -243,6 +246,7 @@ sub test_pg_waldump
'pg_waldump',
'--start' => $start_lsn,
'--end' => $end_lsn,
+ '--path' => $path,
@opts
],
'>' => \$stdout,
@@ -254,11 +258,50 @@ sub test_pg_waldump
return @lines;
}
-my @lines;
+# 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 = sort @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 @scenarios = (
{
- 'path' => $node->data_dir
+ '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 (@scenarios)
@@ -267,6 +310,19 @@ for my $scenario (@scenarios)
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/,
@@ -298,38 +354,42 @@ for my $scenario (@scenarios)
qr/error: error in WAL record at/,
'errors are shown with --quiet');
- @lines = test_pg_waldump('--path' => $path);
+ my @lines;
+ @lines = test_pg_waldump($path);
is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
- @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ @lines = test_pg_waldump($path, '--limit' => 6);
is(@lines, 6, 'limit option observed');
- @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ @lines = test_pg_waldump($path, '--fullpage');
is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
- @lines = test_pg_waldump('--path' => $path, '--stats');
+ @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' => $path, '--stats=record');
+ @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' => $path, '--rmgr' => 'Btree');
+ @lines = test_pg_waldump($path, '--rmgr' => 'Btree');
is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
- @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ @lines = test_pg_waldump($path, '--fork' => 'init');
is(grep(!/fork init/, @lines), 0, 'only init fork lines');
- @lines = test_pg_waldump('--path' => $path,
+ @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' => $path,
+ @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'};
}
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 57a8f0366a5..981cdb69175 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -139,6 +139,8 @@ ArchiveOpts
ArchiveShutdownCB
ArchiveStartupCB
ArchiveStreamState
+ArchivedWALEntry
+ArchivedWAL_hash
ArchiverOutput
ArchiverStage
ArrayAnalyzeExtraData
@@ -3466,6 +3468,7 @@ astreamer_recovery_injector
astreamer_tar_archiver
astreamer_tar_parser
astreamer_verify
+astreamer_waldump
astreamer_zstd_frame
auth_password_hook_typ
autovac_table
--
2.47.1
v9-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchapplication/x-patch; name=v9-0005-pg_waldump-Remove-the-restriction-on-the-order-of.patchDownload
From 4815eb6ef2182b8ef5512bed842aea9821502853 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 6 Nov 2025 13:48:33 +0530
Subject: [PATCH v9 5/8] pg_waldump: Remove the restriction on the order of
archived WAL files.
With previous patch, pg_waldump would stop decoding if WAL files were
not in the required sequence. With this patch, decoding will now
continue. Any WAL file that is out of order will be written to a
temporary location, from which it will be read later. Once a temporary
file has been read, it will be removed.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/archive_waldump.c | 233 ++++++++++++++++++++++++---
src/bin/pg_waldump/pg_waldump.c | 41 ++++-
src/bin/pg_waldump/pg_waldump.h | 4 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
5 files changed, 265 insertions(+), 24 deletions(-)
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index d004bb0f67e..25f96f86168 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -149,8 +149,12 @@ PostgreSQL documentation
of <envar>PGDATA</envar>.
</para>
<para>
- If a tar archive is provided, its WAL segment files must be in
- sequential order; otherwise, an error will be reported.
+ If a tar archive is provided and its WAL segment files are not in
+ sequential order, those files will be written to a temporary directory
+ named starting with <filename>waldump/</filename>. This directory will be
+ created inside the directory specified by the <envar>TMPDIR</envar>
+ environment variable if it is set; otherwise, it will be created within
+ the same directory as the tar archive.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
index 63141dc2ee2..2038876a516 100644
--- a/src/bin/pg_waldump/archive_waldump.c
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -17,6 +17,7 @@
#include <unistd.h>
#include "access/xlog_internal.h"
+#include "common/file_perm.h"
#include "common/hashfn.h"
#include "common/logging.h"
#include "fe_utils/simple_list.h"
@@ -27,6 +28,9 @@
*/
#define READ_CHUNK_SIZE (128 * 1024)
+/* Temporary exported WAL file directory */
+static char *TmpWalSegDir = NULL;
+
/* Structure for storing the WAL segment data from the archive */
typedef struct ArchivedWALEntry
{
@@ -65,6 +69,11 @@ typedef struct astreamer_waldump
static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
XLogDumpPrivate *privateInfo);
+static void setup_tmpseg_dir(const char *waldir);
+static void cleanup_tmpseg_dir_atexit(void);
+
+static FILE *prepare_tmp_write(XLogSegNo segno);
+static void perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file);
static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
static void astreamer_waldump_content(astreamer *streamer,
@@ -120,10 +129,11 @@ is_archive_file(const char *fname, pg_compress_algorithm *compression)
}
/*
- * Initializes the tar archive reader to read WAL files from the archive,
- * creates a hash table to store them, performs quick existence checks for WAL
- * entries in the archive and retrieves the WAL segment size, and sets up
- * filtering criteria for relevant entries.
+ * Initializes the tar archive reader, creates a hash table for WAL entries,
+ * checks for existing valid WAL segments in the archive file and retrieves the
+ * segment size, and sets up filters for relevant entries. It also configures a
+ * temporary directory for out-of-order WAL data and registers an exit callback
+ * to clean up temporary files.
*/
void
init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
@@ -199,6 +209,13 @@ init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
privateInfo->endSegNo = UINT64_MAX;
else
XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+
+ /*
+ * Setup temporary directory to store WAL segments and set up an exit
+ * callback to remove it upon completion.
+ */
+ setup_tmpseg_dir(waldir);
+ atexit(cleanup_tmpseg_dir_atexit);
}
/*
@@ -374,13 +391,16 @@ read_archive_file(XLogDumpPrivate *privateInfo, Size count)
/*
* Returns the archived WAL entry from the hash table if it exists. Otherwise,
* it invokes the routine to read the archived file and retrieve the entry if
- * it is not already in hash table.
+ * it is not already present in the hash table. If the archive streamer happens
+ * to be reading a WAL from archive file that is not currently needed, that WAL
+ * data is written to a temporary file.
*/
static ArchivedWALEntry *
get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
{
ArchivedWALEntry *entry = NULL;
char fname[MAXFNAMELEN];
+ FILE *write_fp = NULL;
/* Search hash table */
entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
@@ -394,8 +414,11 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
entry = privateInfo->cur_wal;
/* Fetch more data */
- if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
- break; /* archive file ended */
+ if (entry == NULL || entry->buf.len == 0)
+ {
+ if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ break; /* archive file ended */
+ }
/*
* Either, here for the first time, or the archived streamer is
@@ -421,20 +444,31 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
}
/*
- * XXX: If the segment being read not the requested one, the data must
- * be buffered, as we currently lack the mechanism to write it to a
- * temporary file. This is a known limitation that will be fixed in the
- * next patch, as the buffer could grow up to the full WAL segment
- * size.
+ * Archive streamer is currently reading a file that isn't the one
+ * asked for, but it's required for a future feature. It should be
+ * written to a temporary location for retrieval when needed.
*/
- if (segno > entry->segno)
- continue;
- /* WAL segments must be archived in order */
- pg_log_error("WAL files are not archived in sequential order");
- pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
- segno, entry->segno);
- exit(1);
+ /* Create a temporary file if one does not already exist */
+ if (!entry->tmpseg_exists)
+ {
+ write_fp = prepare_tmp_write(entry->segno);
+ entry->tmpseg_exists = true;
+ }
+
+ /* Flush data from the buffer to the file */
+ perform_tmp_write(entry->segno, &entry->buf, write_fp);
+ resetStringInfo(&entry->buf);
+
+ /*
+ * The change in the current segment entry indicates that the reading
+ * of this file has ended.
+ */
+ if (entry != privateInfo->cur_wal && write_fp != NULL)
+ {
+ fclose(write_fp);
+ write_fp = NULL;
+ }
}
/* Requested WAL segment not found */
@@ -443,7 +477,166 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
}
/*
- * Create an astreamer that can read WAL from a tar file.
+ * Set up a temporary directory to temporarily store WAL segments.
+ */
+static void
+setup_tmpseg_dir(const char *waldir)
+{
+ char *template;
+
+ /*
+ * Use the directory specified by the TMPDIR environment variable. If it's
+ * not set, use the provided WAL directory to extract WAL file
+ * temporarily.
+ */
+ template = psprintf("%s/waldump_tmp-XXXXXX",
+ getenv("TMPDIR") ? getenv("TMPDIR") : waldir);
+ TmpWalSegDir = mkdtemp(template);
+
+ if (TmpWalSegDir == NULL)
+ pg_fatal("could not create directory \"%s\": %m", template);
+
+ canonicalize_path(TmpWalSegDir);
+
+ pg_log_debug("created directory \"%s\"", TmpWalSegDir);
+}
+
+/*
+ * Removes the temporarily store WAL segments, if any, at exiting.
+ */
+static void
+cleanup_tmpseg_dir_atexit(void)
+{
+ ArchivedWAL_iterator it;
+ ArchivedWALEntry *entry;
+
+ /* Remove temporary segments */
+ ArchivedWAL_start_iterate(ArchivedWAL_HTAB, &it);
+ while ((entry = ArchivedWAL_iterate(ArchivedWAL_HTAB, &it)) != NULL)
+ {
+ if (entry->tmpseg_exists)
+ {
+ remove_tmp_walseg(entry->segno, false);
+ entry->tmpseg_exists = false;
+ }
+ }
+
+ /* Remove temporary directory */
+ if (rmdir(TmpWalSegDir) == 0)
+ pg_log_debug("removed directory \"%s\"", TmpWalSegDir);
+}
+
+/*
+ * Generate the temporary WAL file path.
+ *
+ * Note that the caller is responsible to pfree it.
+ */
+char *
+get_tmp_walseg_path(XLogSegNo segno)
+{
+ char *fpath = (char *) palloc(MAXPGPATH);
+
+ Assert(TmpWalSegDir);
+
+ snprintf(fpath, MAXPGPATH, "%s/%08X%08X",
+ TmpWalSegDir,
+ (uint32) (segno / XLogSegmentsPerXLogId(WalSegSz)),
+ (uint32) (segno % XLogSegmentsPerXLogId(WalSegSz)));
+
+ return fpath;
+}
+
+/*
+ * Routine to check whether a temporary file exists for the corresponding WAL
+ * segment number.
+ */
+bool
+tmp_walseg_exists(XLogSegNo segno)
+{
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry == NULL)
+ return false;
+
+ return entry->tmpseg_exists;
+}
+
+/*
+ * Create an empty placeholder file and return its handle.
+ */
+static FILE *
+prepare_tmp_write(XLogSegNo segno)
+{
+ FILE *file;
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(segno);
+
+ /* 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_debug("temporarily exporting file \"%s\"", fpath);
+ pfree(fpath);
+
+ return file;
+}
+
+/*
+ * Write buffer data to the given file handle.
+ */
+static void
+perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file)
+{
+ Assert(file);
+
+ errno = 0;
+ if (buf->len > 0 && fwrite(buf->data, buf->len, 1, file) != 1)
+ {
+ /*
+ * 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_walseg_path(segno));
+ }
+}
+
+/*
+ * Remove temporary file
+ */
+void
+remove_tmp_walseg(XLogSegNo segno, bool update_entry)
+{
+ char *fpath = get_tmp_walseg_path(segno);
+
+ if (unlink(fpath) == 0)
+ pg_log_debug("removed file \"%s\"", fpath);
+ pfree(fpath);
+
+ /* Update entry if requested */
+ if (update_entry)
+ {
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+ Assert(entry != NULL);
+ entry->tmpseg_exists = false;
+ }
+}
+
+/*
+ * Create an astreamer that can read WAL from tar file.
*/
static astreamer *
astreamer_waldump_new(XLogDumpPrivate *privateInfo)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 1eedf8e01b4..9179f3ea4c4 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -474,12 +474,51 @@ TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
{
XLogDumpPrivate *private = state->private_data;
int count = required_read_len(private, targetPagePtr, reqLen);
+ XLogSegNo nextSegNo;
/* Bail out if the count to be read is not valid */
if (count < 0)
return -1;
- /* Read the WAL page from the archive streamer */
+ /*
+ * 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))
+ {
+ 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. */
+ remove_tmp_walseg(nextSegNo, true);
+ }
+
+ 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
+ */
+ if (tmp_walseg_exists(nextSegNo))
+ {
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(nextSegNo);
+ state->seg.ws_file = open(fpath, O_RDONLY | PG_BINARY, 0);
+ pfree(fpath);
+ }
+ }
+
+ /* Continue reading from the open WAL segment, if any */
+ if (state->seg.ws_file >= 0)
+ return WALDumpReadPage(state, targetPagePtr, count, targetPtr,
+ readBuff);
+
+ /* Otherwise, read the WAL page from the archive streamer */
return read_archive_wal_page(private, targetPagePtr, count, readBuff);
}
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
index ec7a33d40e0..03e02625ba1 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -58,4 +58,8 @@ extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
XLogRecPtr targetPagePtr,
Size count, char *readBuff);
+extern char *get_tmp_walseg_path(XLogSegNo segno);
+extern bool tmp_walseg_exists(XLogSegNo segno);
+extern void remove_tmp_walseg(XLogSegNo segno, bool update_entry);
+
#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 b12bbc6f95b..d752b5dd656 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -7,6 +7,7 @@ use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+use List::Util qw(shuffle);
my $tar = $ENV{TAR};
@@ -272,7 +273,7 @@ sub generate_archive
}
closedir $dh;
- @files = sort @files;
+ @files = shuffle @files;
# move into the WAL directory before archiving files
my $cwd = getcwd;
--
2.47.1
v9-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchapplication/x-patch; name=v9-0006-pg_verifybackup-Delay-default-WAL-directory-prepa.patchDownload
From d4199456b4941dab2dd6dc6326b18045f5297e41 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 14:47:43 +0530
Subject: [PATCH v9 6/8] pg_verifybackup: Delay default WAL directory
preparation.
We are not sure whether to parse WAL from a directory or an archive
until the backup format is known. Therefore, we delay preparing the
default WAL directory until the point of parsing. This delay is
harmless, as the WAL directory is not used elsewhere.
---
src/bin/pg_verifybackup/pg_verifybackup.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 8d5befa947f..a502e795b2e 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -285,10 +285,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
@@ -368,6 +364,10 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
+ /* 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 parse the required ranges of WAL records, unless we were told
* not to do so.
--
2.47.1
v9-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchapplication/x-patch; name=v9-0007-pg_verifybackup-Rename-the-wal-directory-switch-t.patchDownload
From cf5d6388a7ddbb2abf26d7749ff947e3338dbe99 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 25 Nov 2025 17:32:14 +0530
Subject: [PATCH v9 7/8] pg_verifybackup: Rename the wal-directory switch to
wal-path
With previous patches to pg_waldump can now decode WAL directly from
tar files. This means you'll be able to specify a tar archive path
instead of a traditional WAL directory.
To keep things consistent and more versatile, we should also
generalize the input switch for pg_verifybackup. It should accept
either a directory or a tar file path that contains WALs. This change
will also aligning it with the existing manifest-path switch naming.
== NOTE ==
The corresponding PO files require updating due to this change.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 2 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 22 +++++++++++-----------
src/bin/pg_verifybackup/t/007_wal.pl | 4 ++--
3 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index 61c12975e4a..e9b8bfd51b1 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -261,7 +261,7 @@ PostgreSQL documentation
<varlistentry>
<term><option>-w <replaceable class="parameter">path</replaceable></option></term>
- <term><option>--wal-directory=<replaceable class="parameter">path</replaceable></option></term>
+ <term><option>--wal-path=<replaceable class="parameter">path</replaceable></option></term>
<listitem>
<para>
Try to parse WAL files stored in the specified directory, rather than
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index a502e795b2e..9fcd6be004e 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -93,7 +93,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 +126,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 +135,7 @@ 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 *pg_waldump_path = NULL;
DIR *dir;
@@ -221,8 +221,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 */
@@ -365,15 +365,15 @@ main(int argc, char **argv)
verify_backup_checksums(&context);
/* By default, look for the WAL in the backup directory, too. */
- if (wal_directory == NULL)
- wal_directory = psprintf("%s/pg_wal", context.backup_directory);
+ if (wal_path == NULL)
+ wal_path = psprintf("%s/pg_wal", context.backup_directory);
/*
* 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
@@ -1198,7 +1198,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 +1208,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 +1376,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/t/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl
index babc4f0a86b..b07f80719b0 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', '--no-parse-wal', $backup_path ],
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: $!";
--
2.47.1
v9-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchapplication/x-patch; name=v9-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-forma.patchDownload
From b56d51b9094b3e9adeab3480a1c81d47c5a07bbd Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 25 Nov 2025 17:34:26 +0530
Subject: [PATCH v9 8/8] pg_verifybackup: enabled WAL parsing for tar-format
backup
Now that pg_waldump supports decoding from tar archives, we should
leverage this functionality to remove the previous restriction on WAL
parsing for tar-backed formats.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 5 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 66 +++++++++++++------
src/bin/pg_verifybackup/t/002_algorithm.pl | 4 --
src/bin/pg_verifybackup/t/003_corruption.pl | 4 +-
src/bin/pg_verifybackup/t/008_untar.pl | 5 +-
src/bin/pg_verifybackup/t/010_client_untar.pl | 5 +-
6 files changed, 50 insertions(+), 39 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index e9b8bfd51b1..16b50b5a4df 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -36,10 +36,7 @@ PostgreSQL documentation
<literal>backup_manifest</literal> 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 <application>pg_basebackup</application>. However, at present,
- <literal>WAL</literal> verification is supported only for plain-format
- backups. Therefore, if the backup is stored in tar-format, the
- <literal>-n, --no-parse-wal</literal> option should be used.
+ supported by <application>pg_basebackup</application>.
</para>
<para>
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 9fcd6be004e..40ec24c5984 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);
@@ -136,6 +140,8 @@ main(int argc, char **argv)
bool no_parse_wal = false;
bool quiet = false;
char *wal_path = NULL;
+ char *base_archive_path = NULL;
+ char *wal_archive_path = NULL;
char *pg_waldump_path = NULL;
DIR *dir;
@@ -327,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'.
@@ -346,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.
@@ -364,9 +359,28 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
- /* By default, look for the WAL in the backup directory, too. */
+ /*
+ * 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)
- wal_path = psprintf("%s/pg_wal", context.backup_directory);
+ {
+ 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
@@ -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 <tablespaceoid>.tar file here. */
diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl
index ae16c11bc4d..4f284a9e828 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 1dd60f709cf..f1ebdbb46b4 100644
--- a/src/bin/pg_verifybackup/t/003_corruption.pl
+++ b/src/bin/pg_verifybackup/t/003_corruption.pl
@@ -193,10 +193,8 @@ for my $scenario (@scenario)
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/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl
index bc3d6b352ad..09079a94fee 100644
--- a/src/bin/pg_verifybackup/t/008_untar.pl
+++ b/src/bin/pg_verifybackup/t/008_untar.pl
@@ -47,7 +47,6 @@ my $tsoid = $primary->safe_psql(
SELECT oid FROM pg_tablespace WHERE spcname = 'regress_ts1'));
my $backup_path = $primary->backup_dir . '/server-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -123,14 +122,12 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
rmtree($backup_path);
- rmtree($extract_path);
}
}
diff --git a/src/bin/pg_verifybackup/t/010_client_untar.pl b/src/bin/pg_verifybackup/t/010_client_untar.pl
index b62faeb5acf..5b0e76ee69d 100644
--- a/src/bin/pg_verifybackup/t/010_client_untar.pl
+++ b/src/bin/pg_verifybackup/t/010_client_untar.pl
@@ -32,7 +32,6 @@ print $jf $junk_data;
close $jf;
my $backup_path = $primary->backup_dir . '/client-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -137,13 +136,11 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
- rmtree($extract_path);
rmtree($backup_path);
}
}
--
2.47.1
On Nov 26, 2025, at 14:02, Amul Sul <sulamul@gmail.com> wrote:
9 - 0004 ``` +/* + * Create an astreamer that can read WAL from tar file. + */ +static astreamer * +astreamer_waldump_new(XLogDumpPrivate *privateInfo) +{ + astreamer_waldump *streamer; + + streamer = palloc0(sizeof(astreamer_waldump)); + *((const astreamer_ops **) &streamer->base.bbs_ops) = + &astreamer_waldump_ops; + + streamer->privateInfo = privateInfo; + + return &streamer->base; +} ```This function allocates memory for streamer but only returns &streamer->base, so memory of streamer is leaked.
May I know why you think there would be a memory leak? I believe the
address of the structure is the same as the address of its first
member, base. I am returning base because the goal is to return a
generic astreamer type, which is the standard approach used in other
archive streamer code.
Ah… Got it.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
Attached is the rebased version, includes some code comment improvements.
Regards,
Amul
Attachments:
v10-0001-Refactor-pg_waldump-Move-some-declarations-to-ne.patchapplication/octet-stream; name=v10-0001-Refactor-pg_waldump-Move-some-declarations-to-ne.patchDownload
From 6e0f330b88eb027c36b1272b0bb7eccb1c50e39a Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 24 Jun 2025 11:33:20 +0530
Subject: [PATCH v10 1/8] Refactor: pg_waldump: Move some declarations to new
pg_waldump.h
This change prepares for a second source file in this directory to
support reading WAL from tar files. Common structures, declarations,
and functions are being exported through this include file so
they can be used in both files.
---
src/bin/pg_waldump/pg_waldump.c | 12 +++---------
src/bin/pg_waldump/pg_waldump.h | 27 +++++++++++++++++++++++++++
2 files changed, 30 insertions(+), 9 deletions(-)
create mode 100644 src/bin/pg_waldump/pg_waldump.h
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index aae655966ef..0a0e9b7caaf 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"
@@ -37,21 +38,14 @@
* give a thought about doing the same in pg_walinspect contrib module as well.
*/
+int WalSegSz;
+
static const char *progname;
-static int WalSegSz;
static volatile sig_atomic_t time_to_stop = false;
static const RelFileLocator emptyRelFileLocator = {0, 0, 0};
-typedef struct XLogDumpPrivate
-{
- TimeLineID timeline;
- XLogRecPtr startptr;
- XLogRecPtr endptr;
- bool endptr_reached;
-} XLogDumpPrivate;
-
typedef struct XLogDumpConfig
{
/* display options */
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
new file mode 100644
index 00000000000..926d529f9d6
--- /dev/null
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -0,0 +1,27 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_waldump.h - decode and display WAL
+ *
+ * Copyright (c) 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"
+
+extern int WalSegSz;
+
+/* Contains the necessary information to drive WAL decoding */
+typedef struct XLogDumpPrivate
+{
+ TimeLineID timeline;
+ XLogRecPtr startptr;
+ XLogRecPtr endptr;
+ bool endptr_reached;
+} XLogDumpPrivate;
+
+#endif /* end of PG_WALDUMP_H */
--
2.47.1
v10-0002-Refactor-pg_waldump-Separate-logic-used-to-calcu.patchapplication/octet-stream; name=v10-0002-Refactor-pg_waldump-Separate-logic-used-to-calcu.patchDownload
From cd4adf2105028a44527fe3fbbea65153de8d8a4c Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 26 Jun 2025 11:42:53 +0530
Subject: [PATCH v10 2/8] Refactor: pg_waldump: Separate logic used to
calculate the required read size.
This refactoring prepares the codebase for an upcoming patch that will
support reading WAL from tar files. The logic for calculating the
required read size has been updated to handle both normal WAL files
and WAL files located inside a tar archive.
---
src/bin/pg_waldump/pg_waldump.c | 46 +++++++++++++++++++++++----------
1 file changed, 33 insertions(+), 13 deletions(-)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 0a0e9b7caaf..a33fc28b9d5 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -327,6 +327,35 @@ identify_target_directory(char *directory, char *fname)
return NULL; /* not reached */
}
+/*
+ * Returns the size in bytes of the data to be read. Returns -1 if the end
+ * point has already been reached.
+ */
+static inline int
+required_read_len(XLogDumpPrivate *private, XLogRecPtr targetPagePtr,
+ int reqLen)
+{
+ int count = XLOG_BLCKSZ;
+
+ if (unlikely(private->endptr_reached))
+ return -1;
+
+ if (XLogRecPtrIsValid(private->endptr))
+ {
+ 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,
@@ -384,21 +413,12 @@ 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 (XLogRecPtrIsValid(private->endptr))
- {
- 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;
- }
- }
+ /* Bail out if the count to be read is not valid */
+ if (count < 0)
+ return -1;
if (!WALRead(state, readBuff, targetPagePtr, count, private->timeline,
&errinfo))
--
2.47.1
v10-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchapplication/octet-stream; name=v10-0003-Refactor-pg_waldump-Restructure-TAP-tests.patchDownload
From f911d0ff34bc2c2e12fb0849785d2b5864c73594 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 25 Nov 2025 16:12:11 +0530
Subject: [PATCH v10 3/8] Refactor: pg_waldump: Restructure TAP tests.
Restructured some tests to run inside a loop, facilitating their
re-execution for decoding WAL from tar archives.
== NOTE ==
This is not intended to be committed separately. It can be merged
with the next patch, which is the main patch implementing this
feature.
---
src/bin/pg_waldump/t/001_basic.pl | 123 ++++++++++++++++--------------
1 file changed, 67 insertions(+), 56 deletions(-)
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 5db5d20136f..3288fadcf48 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -198,28 +198,6 @@ command_like(
],
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 +205,6 @@ command_like(
],
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.
@@ -272,7 +241,6 @@ sub test_pg_waldump
my $result = IPC::Run::run [
'pg_waldump',
- '--path' => $node->data_dir,
'--start' => $start_lsn,
'--end' => $end_lsn,
@opts
@@ -288,38 +256,81 @@ sub test_pg_waldump
my @lines;
-@lines = test_pg_waldump;
-is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
+my @scenarios = (
+ {
+ 'path' => $node->data_dir
+ });
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenarios)
+{
+ my $path = $scenario->{'path'};
-@lines = test_pg_waldump('--fullpage');
-is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
+ SKIP:
+ {
+ 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');
-@lines = test_pg_waldump('--stats');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+ command_fails_like(
+ [
+ 'pg_waldump', '--quiet',
+ '--path' => $path,
+ '--start' => $start_lsn
+ ],
+ qr/error: error in WAL record at/,
+ 'errors are shown with --quiet');
-@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('--path' => $path);
+ is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
-@lines = test_pg_waldump('--rmgr' => 'Btree');
-is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+ @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ is(@lines, 6, 'limit option observed');
-@lines = test_pg_waldump('--fork' => 'init');
-is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+ @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
-@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('--path' => $path, '--stats');
+ like($lines[0], qr/WAL statistics/, "statistics on stdout");
+ is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
-@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');
+ @lines = test_pg_waldump('--path' => $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' => $path, '--rmgr' => 'Btree');
+ is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+
+ @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+
+ @lines = test_pg_waldump('--path' => $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' => $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');
+ }
+}
done_testing();
--
2.47.1
v10-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchapplication/octet-stream; name=v10-0004-pg_waldump-Add-support-for-archived-WAL-decoding.patchDownload
From b8fba73c2abf78fd6972ac077b3d22332a4d477b Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Fri, 2 Jan 2026 17:07:34 +0530
Subject: [PATCH v10 4/8] pg_waldump: Add support for archived WAL decoding.
pg_waldump can now accept the path to a tar archive containing WAL
files and decode them. This feature was added primarily for
pg_verifybackup, which previously disabled WAL parsing for
tar-formatted backups.
Note that this patch requires that the WAL files within the archive be
in sequential order; an error will be reported otherwise. The next
patch is planned to remove this restriction.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/Makefile | 7 +-
src/bin/pg_waldump/archive_waldump.c | 594 +++++++++++++++++++++++++++
src/bin/pg_waldump/meson.build | 4 +-
src/bin/pg_waldump/pg_waldump.c | 218 +++++++---
src/bin/pg_waldump/pg_waldump.h | 34 ++
src/bin/pg_waldump/t/001_basic.pl | 84 +++-
src/tools/pgindent/typedefs.list | 3 +
8 files changed, 876 insertions(+), 76 deletions(-)
create mode 100644 src/bin/pg_waldump/archive_waldump.c
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index ce23add5577..d004bb0f67e 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -141,13 +141,17 @@ PostgreSQL documentation
<term><option>--path=<replaceable>path</replaceable></option></term>
<listitem>
<para>
- Specifies a directory to search for WAL segment files or a
- directory with a <literal>pg_wal</literal> subdirectory that
+ Specifies a tar archive or a directory to search for WAL segment files
+ or a directory with a <literal>pg_wal</literal> subdirectory that
contains such files. The default is to search in the current
directory, the <literal>pg_wal</literal> subdirectory of the
current directory, and the <literal>pg_wal</literal> subdirectory
of <envar>PGDATA</envar>.
</para>
+ <para>
+ If a tar archive is provided, its WAL segment files must be in
+ sequential order; otherwise, an error will be reported.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile
index 4c1ee649501..aabb87566a2 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
@@ -10,13 +13,15 @@ include $(top_builddir)/src/Makefile.global
OBJS = \
$(RMGRDESCOBJS) \
$(WIN32RES) \
+ archive_waldump.o \
compat.o \
pg_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/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
new file mode 100644
index 00000000000..6ba067163d5
--- /dev/null
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -0,0 +1,594 @@
+/*-------------------------------------------------------------------------
+ *
+ * archive_waldump.c
+ * A generic facility for reading WAL data from tar archives via archive
+ * streamer.
+ *
+ * Portions Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ * src/bin/pg_waldump/archive_waldump.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <unistd.h>
+
+#include "access/xlog_internal.h"
+#include "common/hashfn.h"
+#include "common/logging.h"
+#include "fe_utils/simple_list.h"
+#include "pg_waldump.h"
+
+/*
+ * How many bytes should we try to read from a file at once?
+ */
+#define READ_CHUNK_SIZE (128 * 1024)
+
+/* Hash entry structure for holding WAL segment data read from the archive */
+typedef struct ArchivedWALEntry
+{
+ uint32 status; /* hash status */
+ XLogSegNo segno; /* hash key: WAL segment number */
+ TimeLineID timeline; /* timeline of this wal file */
+
+ StringInfoData buf;
+ bool tmpseg_exists; /* spill file exists? */
+
+ int total_read; /* total read of archived WAL segment */
+} ArchivedWALEntry;
+
+#define SH_PREFIX ArchivedWAL
+#define SH_ELEMENT_TYPE ArchivedWALEntry
+#define SH_KEY_TYPE XLogSegNo
+#define SH_KEY segno
+#define SH_HASH_KEY(tb, key) murmurhash64((uint64) key)
+#define SH_EQUAL(tb, a, b) (a == b)
+#define SH_GET_HASH(tb, a) a->hash
+#define SH_SCOPE static inline
+#define SH_RAW_ALLOCATOR pg_malloc0
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
+
+static ArchivedWAL_hash *ArchivedWAL_HTAB = NULL;
+
+typedef struct astreamer_waldump
+{
+ astreamer base;
+ XLogDumpPrivate *privateInfo;
+} astreamer_waldump;
+
+static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
+static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
+ XLogDumpPrivate *privateInfo);
+
+static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
+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_wal_file(astreamer_waldump *mystreamer,
+ astreamer_member *member,
+ XLogSegNo *curSegNo,
+ TimeLineID *curTimeline);
+
+static const astreamer_ops astreamer_waldump_ops = {
+ .content = astreamer_waldump_content,
+ .finalize = astreamer_waldump_finalize,
+ .free = astreamer_waldump_free
+};
+
+/*
+ * Returns true if the given file is a tar archive and outputs its compression
+ * algorithm.
+ */
+bool
+is_archive_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;
+}
+
+/*
+ * Initializes the tar archive reader, creates a hash table for WAL entries,
+ * checks for existing valid WAL segments in the archive file and retrieves the
+ * segment size, and sets up filters for relevant entries.
+ */
+void
+init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
+ pg_compress_algorithm compression)
+{
+ int fd;
+ astreamer *streamer;
+ ArchivedWALEntry *entry = NULL;
+ XLogLongPageHeader longhdr;
+
+ /* Open tar archive and store its file descriptor */
+ fd = open_file_in_directory(waldir, privateInfo->archive_name);
+
+ if (fd < 0)
+ pg_fatal("could not open file \"%s\"", privateInfo->archive_name);
+
+ privateInfo->archive_fd = fd;
+
+ streamer = astreamer_waldump_new(privateInfo);
+
+ /* 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);
+
+ privateInfo->archive_streamer = streamer;
+
+ /* Hash table storing WAL entries read from the archive */
+ ArchivedWAL_HTAB = ArchivedWAL_create(16, NULL);
+
+ /*
+ * Verify that the archive contains valid WAL files and fetch WAL segment
+ * size
+ */
+ while (entry == NULL || entry->buf.len < XLOG_BLCKSZ)
+ {
+ if (read_archive_file(privateInfo, XLOG_BLCKSZ) == 0)
+ pg_fatal("could not find WAL in \"%s\" archive",
+ privateInfo->archive_name);
+
+ entry = privateInfo->cur_wal;
+ }
+
+ /* Set WalSegSz if WAL data is successfully read */
+ longhdr = (XLogLongPageHeader) entry->buf.data;
+
+ WalSegSz = longhdr->xlp_seg_size;
+
+ if (!IsValidWalSegSize(WalSegSz))
+ {
+ pg_log_error(ngettext("invalid WAL segment size in WAL file from archive \"%s\" (%d byte)",
+ "invalid WAL segment size in WAL file from archive \"%s\" (%d bytes)",
+ WalSegSz),
+ privateInfo->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);
+ }
+
+ /*
+ * With the WAL segment size available, we can now initialize the
+ * dependent start and end segment numbers.
+ */
+ Assert(!XLogRecPtrIsInvalid(privateInfo->startptr));
+ XLByteToSeg(privateInfo->startptr, privateInfo->startSegNo, WalSegSz);
+
+ if (XLogRecPtrIsInvalid(privateInfo->endptr))
+ privateInfo->endSegNo = UINT64_MAX;
+ else
+ XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+}
+
+/*
+ * Release the archive streamer chain and close the archive file.
+ */
+void
+free_archive_reader(XLogDumpPrivate *privateInfo)
+{
+ /*
+ * 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(privateInfo->archive_streamer);
+
+ /* Close the file. */
+ if (close(privateInfo->archive_fd) != 0)
+ pg_log_error("could not close file \"%s\": %m",
+ privateInfo->archive_name);
+}
+
+/*
+ * Copies WAL data from astreamer to readBuff; if unavailable, fetches more
+ * from the tar archive via astreamer.
+ */
+int
+read_archive_wal_page(XLogDumpPrivate *privateInfo, XLogRecPtr targetPagePtr,
+ Size count, char *readBuff)
+{
+ char *p = readBuff;
+ Size nbytes = count;
+ XLogRecPtr recptr = targetPagePtr;
+ XLogSegNo segno;
+ ArchivedWALEntry *entry;
+
+ XLByteToSeg(targetPagePtr, segno, WalSegSz);
+ entry = get_archive_wal_entry(segno, privateInfo);
+
+ while (nbytes > 0)
+ {
+ char *buf = entry->buf.data;
+ int len = entry->buf.len;
+
+ /* WAL record range that the buffer contains */
+ XLogRecPtr endPtr;
+ XLogRecPtr startPtr;
+
+ XLogSegNoOffsetToRecPtr(entry->segno, entry->total_read,
+ WalSegSz, endPtr);
+ startPtr = endPtr - len;
+
+ /*
+ * pg_waldump may request to re-read the currently active page, but
+ * never a page older than the current one. Therefore, any fully
+ * consumed WAL data preceding the current page can be safely
+ * discarded.
+ */
+ if (recptr >= endPtr)
+ {
+ /* Discard the buffered data */
+ resetStringInfo(&entry->buf);
+ len = 0;
+
+ /*
+ * Push back the partial page data for the current page to the
+ * buffer, ensuring it remains full page available for re-reading
+ * if requested.
+ */
+ if (p > readBuff)
+ {
+ Assert((count - nbytes) > 0);
+ appendBinaryStringInfo(&entry->buf, readBuff, count - nbytes);
+ }
+ }
+
+ if (len > 0 && recptr > startPtr)
+ {
+ int skipBytes = 0;
+
+ /*
+ * The required offset is not at the start of the 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 the reading page is in the buffer */
+ Assert(recptr >= startPtr && recptr < endPtr);
+
+ memcpy(p, buf, readBytes);
+
+ /* Update state for read */
+ nbytes -= readBytes;
+ p += readBytes;
+ recptr += readBytes;
+ }
+ else
+ {
+ /*
+ * Fetch more data; raise an error if it's not the current segment
+ * being read by the archive streamer or if reading of the
+ * archived file has finished.
+ */
+ if (privateInfo->cur_wal != entry ||
+ read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ {
+ char fname[MAXFNAMELEN];
+
+ XLogFileName(fname, privateInfo->timeline, entry->segno,
+ WalSegSz);
+ pg_fatal("could not read file \"%s\" from archive \"%s\": read %lld of %lld",
+ fname, privateInfo->archive_name,
+ (long long int) count - nbytes,
+ (long long int) nbytes);
+ }
+ }
+ }
+
+ /*
+ * Should have either have successfully read all the requested bytes or
+ * reported a failure before this point.
+ */
+ Assert(nbytes == 0);
+
+ /*
+ * NB: We return the fixed value provided as input. Although we could
+ * return a boolean since we either successfully read the WAL page or
+ * raise an error, but the caller expects this value to be returned. The
+ * routine that reads WAL pages from the physical WAL file follows the
+ * same convention.
+ */
+ return count;
+}
+
+/*
+ * Reads the archive file and passes it to the archive streamer for
+ * decompression.
+ */
+static int
+read_archive_file(XLogDumpPrivate *privateInfo, Size count)
+{
+ int rc;
+ char *buffer;
+
+ buffer = pg_malloc(READ_CHUNK_SIZE * sizeof(uint8));
+
+ rc = read(privateInfo->archive_fd, buffer, count);
+ if (rc < 0)
+ pg_fatal("could not read file \"%s\": %m",
+ privateInfo->archive_name);
+
+ /*
+ * Decompress (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;
+}
+
+/*
+ * Returns the archived WAL entry from the hash table if it exists. Otherwise,
+ * it invokes the routine to read the archived file, which then populates the
+ * entry in the hash table.
+ */
+static ArchivedWALEntry *
+get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
+{
+ ArchivedWALEntry *entry = NULL;
+ char fname[MAXFNAMELEN];
+
+ /* Search hash table */
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry != NULL)
+ return entry;
+
+ /* Needed WAL yet to be decoded from archive, do the same */
+ while (1)
+ {
+ entry = privateInfo->cur_wal;
+
+ /* Fetch more data */
+ if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ break; /* archive file ended */
+
+ /*
+ * Either, here for the first time, or the archived streamer is
+ * reading a non-WAL file or an irrelevant WAL file.
+ */
+ if (entry == NULL)
+ continue;
+
+ /* Found the required entry */
+ if (entry->segno == segno)
+ return entry;
+
+ /*
+ * Ignore if the timeline is different or the current segment is not
+ * the desired one.
+ */
+ if (privateInfo->timeline != entry->timeline ||
+ privateInfo->startSegNo > entry->segno ||
+ privateInfo->endSegNo < entry->segno)
+ {
+ privateInfo->cur_wal = NULL;
+ continue;
+ }
+
+ /*
+ * XXX: If the segment being read not the requested one, the data must
+ * be buffered, as we currently lack the mechanism to write it to a
+ * temporary file. This is a known limitation that will be fixed in the
+ * next patch, as the buffer could grow up to the full WAL segment
+ * size.
+ */
+ if (segno > entry->segno)
+ continue;
+
+ /* WAL segments must be archived in order */
+ pg_log_error("WAL files are not archived in sequential order");
+ pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
+ segno, entry->segno);
+ exit(1);
+ }
+
+ /* Requested WAL segment not found */
+ XLogFileName(fname, privateInfo->timeline, segno, WalSegSz);
+ pg_fatal("could not find file \"%s\" in archive", fname);
+}
+
+/*
+ * Create an astreamer that can read WAL from a tar file.
+ */
+static astreamer *
+astreamer_waldump_new(XLogDumpPrivate *privateInfo)
+{
+ astreamer_waldump *streamer;
+
+ streamer = palloc0(sizeof(astreamer_waldump));
+ *((const astreamer_ops **) &streamer->base.bbs_ops) =
+ &astreamer_waldump_ops;
+
+ streamer->privateInfo = privateInfo;
+
+ return &streamer->base;
+}
+
+/*
+ * Main entry point of the archive streamer for reading WAL data from a tar
+ * file. If a member is identified as a valid WAL file, a hash entry is created
+ * for it, and its contents are copied into that entry's buffer, making them
+ * accessible to the decoding routine.
+ */
+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:
+ {
+ XLogSegNo segno;
+ TimeLineID timeline;
+ ArchivedWALEntry *entry;
+ bool found;
+
+ pg_log_debug("reading \"%s\"", member->pathname);
+
+ if (!member_is_wal_file(mystreamer, member,
+ &segno, &timeline))
+ break;
+
+ entry = ArchivedWAL_insert(ArchivedWAL_HTAB, segno, &found);
+
+ /*
+ * Shouldn't happen, but if it does, simply ignore the
+ * duplicate WAL file.
+ */
+ if (found)
+ {
+ pg_log_warning("ignoring duplicate WAL file found in archive: \"%s\"",
+ member->pathname);
+ break;
+ }
+
+ initStringInfo(&entry->buf);
+ entry->timeline = timeline;
+ entry->total_read = 0;
+
+ privateInfo->cur_wal = entry;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_CONTENTS:
+ if (privateInfo->cur_wal)
+ {
+ appendBinaryStringInfo(&privateInfo->cur_wal->buf, data, len);
+ privateInfo->cur_wal->total_read += len;
+ }
+ break;
+
+ case ASTREAMER_MEMBER_TRAILER:
+ privateInfo->cur_wal = NULL;
+ break;
+
+ case ASTREAMER_ARCHIVE_TRAILER:
+ break;
+
+ default:
+ /* Shouldn't happen. */
+ pg_fatal("unexpected state while parsing tar file");
+ }
+}
+
+/*
+ * End-of-stream processing for an 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)
+{
+ Assert(streamer->bbs_next == NULL);
+ pfree(streamer);
+}
+
+/*
+ * Returns true if the archive member name matches the WAL naming format. If
+ * successful, it also outputs the WAL segment number, and timeline.
+ */
+static bool
+member_is_wal_file(astreamer_waldump *mystreamer, astreamer_member *member,
+ XLogSegNo *curSegNo, TimeLineID *curTimeline)
+{
+ 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;
+
+ /*
+ * XXX: On some systems (e.g., OpenBSD), the tar utility includes
+ * PaxHeaders when creating an archive. These are special entries that
+ * store extended metadata for the file entry immediately following them,
+ * and they share the exact same name as that file.
+ */
+ if (strstr(member->pathname, "PaxHeaders."))
+ return false;
+
+ /* Parse position from file */
+ XLogFromFileName(fname, &timeline, &segNo, WalSegSz);
+
+ *curSegNo = segNo;
+ *curTimeline = timeline;
+
+ return true;
+}
diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build
index 633a9874bb5..a93bd947ca2 100644
--- a/src/bin/pg_waldump/meson.build
+++ b/src/bin/pg_waldump/meson.build
@@ -1,6 +1,7 @@
# Copyright (c) 2022-2026, PostgreSQL Global Development Group
pg_waldump_sources = files(
+ 'archive_waldump.c',
'compat.c',
'pg_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 a33fc28b9d5..e2c96c3f4ca 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -38,7 +38,7 @@
* give a thought about doing the same in pg_walinspect contrib module as well.
*/
-int WalSegSz;
+int WalSegSz = DEFAULT_XLOG_SEG_SIZE;
static const char *progname;
@@ -178,7 +178,7 @@ split_path(const char *path, char **dir, char **fname)
*
* return a read only fd
*/
-static int
+int
open_file_in_directory(const char *directory, const char *fname)
{
int fd = -1;
@@ -444,6 +444,45 @@ 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);
+
+ /* Bail out if the count to be read is not valid */
+ if (count < 0)
+ return -1;
+
+ /* Read the WAL page from the archive streamer */
+ return read_archive_wal_page(private, targetPagePtr, count, readBuff);
+}
+
/*
* Boolean to return whether the given WAL record matches a specific relation
* and optionally block.
@@ -781,8 +820,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"
@@ -814,7 +853,10 @@ main(int argc, char **argv)
XLogRecord *record;
XLogRecPtr first_record;
char *waldir = NULL;
+ char *walpath = NULL;
char *errormsg;
+ bool is_archive = false;
+ pg_compress_algorithm compression;
static struct option long_options[] = {
{"bkp-details", no_argument, NULL, 'b'},
@@ -946,7 +988,7 @@ main(int argc, char **argv)
}
break;
case 'p':
- waldir = pg_strdup(optarg);
+ walpath = pg_strdup(optarg);
break;
case 'q':
config.quiet = true;
@@ -1110,10 +1152,20 @@ main(int argc, char **argv)
goto bad_argument;
}
- if (waldir != NULL)
+ if (walpath != NULL)
{
+ /* validate path points to tar archive */
+ if (is_archive_file(walpath, &compression))
+ {
+ char *fname = NULL;
+
+ split_path(walpath, &waldir, &fname);
+
+ private.archive_name = fname;
+ is_archive = 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;
@@ -1131,6 +1183,17 @@ 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_archive)
+ {
+ 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)
@@ -1141,69 +1204,77 @@ main(int argc, char **argv)
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 (!XLogRecPtrIsValid(private.startptr))
- XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr);
- else if (!XLByteInSeg(private.startptr, segno, WalSegSz))
+ if (fname != NULL && is_archive_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_archive = true;
}
-
- /* no second file specified, set end position */
- if (!(optind + 1 < argc) && !XLogRecPtrIsValid(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(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, &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 (!XLogRecPtrIsValid(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 (!XLogRecPtrIsValid(private.endptr))
- XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz,
- private.endptr);
+ /* no second file specified, set end position */
+ if (!(optind + 1 < argc) && !XLogRecPtrIsValid(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 (!XLogRecPtrIsValid(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_archive)
+ waldir = identify_target_directory(walpath, NULL);
/* we don't know what to print */
if (!XLogRecPtrIsValid(private.startptr))
@@ -1215,12 +1286,36 @@ 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_archive)
+ {
+ /*
+ * A NULL WAL directory indicates that the archive file is located in
+ * the current working directory of the pg_waldump execution
+ */
+ waldir = waldir ? pg_strdup(waldir) : pg_strdup(".");
+
+ /* Set up for reading tar file */
+ init_archive_reader(&private, waldir, compression);
+
+ /* 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");
@@ -1329,6 +1424,9 @@ main(int argc, char **argv)
XLogReaderFree(xlogreader_state);
+ if (is_archive)
+ free_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
index 926d529f9d6..ec7a33d40e0 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -12,9 +12,13 @@
#define PG_WALDUMP_H
#include "access/xlogdefs.h"
+#include "fe_utils/astreamer.h"
extern int WalSegSz;
+/* Forward declaration */
+struct ArchivedWALEntry;
+
/* Contains the necessary information to drive WAL decoding */
typedef struct XLogDumpPrivate
{
@@ -22,6 +26,36 @@ typedef struct XLogDumpPrivate
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;
+
+ /* What the archive streamer is currently reading */
+ struct ArchivedWALEntry *cur_wal;
+
+ /*
+ * Although these values can be easily derived from startptr and endptr,
+ * doing so repeatedly for each archived member would be inefficient, as
+ * it would involve recalculating and filtering out irrelevant WAL
+ * segments.
+ */
+ XLogSegNo startSegNo;
+ XLogSegNo endSegNo;
} XLogDumpPrivate;
+extern int open_file_in_directory(const char *directory, const char *fname);
+
+extern bool is_archive_file(const char *fname,
+ pg_compress_algorithm *compression);
+extern void init_archive_reader(XLogDumpPrivate *privateInfo,
+ const char *waldir,
+ pg_compress_algorithm compression);
+extern void free_archive_reader(XLogDumpPrivate *privateInfo);
+extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
+ XLogRecPtr targetPagePtr,
+ Size count, char *readBuff);
+
#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 3288fadcf48..13567fbdba1 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -3,10 +3,13 @@
use strict;
use warnings FATAL => 'all';
+use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $tar = $ENV{TAR};
+
program_help_ok('pg_waldump');
program_version_ok('pg_waldump');
program_options_handling_ok('pg_waldump');
@@ -235,7 +238,7 @@ command_like(
sub test_pg_waldump
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my @opts = @_;
+ my ($path, @opts) = @_;
my ($stdout, $stderr);
@@ -243,6 +246,7 @@ sub test_pg_waldump
'pg_waldump',
'--start' => $start_lsn,
'--end' => $end_lsn,
+ '--path' => $path,
@opts
],
'>' => \$stdout,
@@ -254,11 +258,50 @@ sub test_pg_waldump
return @lines;
}
-my @lines;
+# 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 = sort @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 @scenarios = (
{
- 'path' => $node->data_dir
+ '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 (@scenarios)
@@ -267,6 +310,19 @@ for my $scenario (@scenarios)
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/,
@@ -298,38 +354,42 @@ for my $scenario (@scenarios)
qr/error: error in WAL record at/,
'errors are shown with --quiet');
- @lines = test_pg_waldump('--path' => $path);
+ my @lines;
+ @lines = test_pg_waldump($path);
is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
- @lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+ @lines = test_pg_waldump($path, '--limit' => 6);
is(@lines, 6, 'limit option observed');
- @lines = test_pg_waldump('--path' => $path, '--fullpage');
+ @lines = test_pg_waldump($path, '--fullpage');
is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
- @lines = test_pg_waldump('--path' => $path, '--stats');
+ @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' => $path, '--stats=record');
+ @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' => $path, '--rmgr' => 'Btree');
+ @lines = test_pg_waldump($path, '--rmgr' => 'Btree');
is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
- @lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+ @lines = test_pg_waldump($path, '--fork' => 'init');
is(grep(!/fork init/, @lines), 0, 'only init fork lines');
- @lines = test_pg_waldump('--path' => $path,
+ @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' => $path,
+ @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'};
}
}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b9e671fcda8..b0d73743cea 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -144,6 +144,8 @@ ArchiveOpts
ArchiveShutdownCB
ArchiveStartupCB
ArchiveStreamState
+ArchivedWALEntry
+ArchivedWAL_hash
ArchiverOutput
ArchiverStage
ArrayAnalyzeExtraData
@@ -3495,6 +3497,7 @@ astreamer_recovery_injector
astreamer_tar_archiver
astreamer_tar_parser
astreamer_verify
+astreamer_waldump
astreamer_zstd_frame
auth_password_hook_typ
autovac_table
--
2.47.1
v10-0005-pg_waldump-Remove-the-restriction-on-the-order-o.patchapplication/octet-stream; name=v10-0005-pg_waldump-Remove-the-restriction-on-the-order-o.patchDownload
From c2811b147d50a7e2e8f6566344e9f1f85614560d Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Thu, 6 Nov 2025 13:48:33 +0530
Subject: [PATCH v10 5/8] pg_waldump: Remove the restriction on the order of
archived WAL files.
With previous patch, pg_waldump would stop decoding if WAL files were
not in the required sequence. With this patch, decoding will now
continue. Any WAL file that is out of order will be written to a
temporary location, from which it will be read later. Once a temporary
file has been read, it will be removed.
---
doc/src/sgml/ref/pg_waldump.sgml | 8 +-
src/bin/pg_waldump/archive_waldump.c | 230 ++++++++++++++++++++++++---
src/bin/pg_waldump/pg_waldump.c | 41 ++++-
src/bin/pg_waldump/pg_waldump.h | 4 +
src/bin/pg_waldump/t/001_basic.pl | 3 +-
5 files changed, 264 insertions(+), 22 deletions(-)
diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index d004bb0f67e..27adf77755c 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -149,8 +149,12 @@ PostgreSQL documentation
of <envar>PGDATA</envar>.
</para>
<para>
- If a tar archive is provided, its WAL segment files must be in
- sequential order; otherwise, an error will be reported.
+ If a tar archive is provided and its WAL segment files are not in
+ sequential order, those files will be written to a temporary directory
+ named starting with <filename>waldump_tmp</filename>. This directory will be
+ created inside the directory specified by the <envar>TMPDIR</envar>
+ environment variable if it is set; otherwise, it will be created within
+ the same directory as the tar archive.
</para>
</listitem>
</varlistentry>
diff --git a/src/bin/pg_waldump/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c
index 6ba067163d5..aa64da9a57d 100644
--- a/src/bin/pg_waldump/archive_waldump.c
+++ b/src/bin/pg_waldump/archive_waldump.c
@@ -17,6 +17,7 @@
#include <unistd.h>
#include "access/xlog_internal.h"
+#include "common/file_perm.h"
#include "common/hashfn.h"
#include "common/logging.h"
#include "fe_utils/simple_list.h"
@@ -27,6 +28,9 @@
*/
#define READ_CHUNK_SIZE (128 * 1024)
+/* Temporary exported WAL file directory */
+static char *TmpWalSegDir = NULL;
+
/* Hash entry structure for holding WAL segment data read from the archive */
typedef struct ArchivedWALEntry
{
@@ -64,6 +68,11 @@ typedef struct astreamer_waldump
static int read_archive_file(XLogDumpPrivate *privateInfo, Size count);
static ArchivedWALEntry *get_archive_wal_entry(XLogSegNo segno,
XLogDumpPrivate *privateInfo);
+static void setup_tmpseg_dir(const char *waldir);
+static void cleanup_tmpseg_dir_atexit(void);
+
+static FILE *prepare_tmp_write(XLogSegNo segno);
+static void perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file);
static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo);
static void astreamer_waldump_content(astreamer *streamer,
@@ -121,7 +130,9 @@ is_archive_file(const char *fname, pg_compress_algorithm *compression)
/*
* Initializes the tar archive reader, creates a hash table for WAL entries,
* checks for existing valid WAL segments in the archive file and retrieves the
- * segment size, and sets up filters for relevant entries.
+ * segment size, and sets up filters for relevant entries. It also configures a
+ * temporary directory for out-of-order WAL data and registers an exit callback
+ * to clean up temporary files.
*/
void
init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
@@ -197,6 +208,13 @@ init_archive_reader(XLogDumpPrivate *privateInfo, const char *waldir,
privateInfo->endSegNo = UINT64_MAX;
else
XLByteToSeg(privateInfo->endptr, privateInfo->endSegNo, WalSegSz);
+
+ /*
+ * Setup temporary directory to store WAL segments and set up an exit
+ * callback to remove it upon completion.
+ */
+ setup_tmpseg_dir(waldir);
+ atexit(cleanup_tmpseg_dir_atexit);
}
/*
@@ -370,15 +388,18 @@ read_archive_file(XLogDumpPrivate *privateInfo, Size count)
}
/*
- * Returns the archived WAL entry from the hash table if it exists. Otherwise,
+ * Returns the archived WAL entry from the hash table if it exists. Otherwise,
* it invokes the routine to read the archived file, which then populates the
- * entry in the hash table.
+ * entry in the hash table. If the archive streamer happens to be reading a
+ * WAL from archive file that is not currently needed, that WAL data is written
+ * to a temporary file.
*/
static ArchivedWALEntry *
get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
{
ArchivedWALEntry *entry = NULL;
char fname[MAXFNAMELEN];
+ FILE *write_fp = NULL;
/* Search hash table */
entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
@@ -392,8 +413,11 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
entry = privateInfo->cur_wal;
/* Fetch more data */
- if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
- break; /* archive file ended */
+ if (entry == NULL || entry->buf.len == 0)
+ {
+ if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0)
+ break; /* archive file ended */
+ }
/*
* Either, here for the first time, or the archived streamer is
@@ -419,20 +443,31 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
}
/*
- * XXX: If the segment being read not the requested one, the data must
- * be buffered, as we currently lack the mechanism to write it to a
- * temporary file. This is a known limitation that will be fixed in the
- * next patch, as the buffer could grow up to the full WAL segment
- * size.
+ * Archive streamer is currently reading a file that isn't the one
+ * asked for, but it's required in the future. It should be written to
+ * a temporary location for retrieval when needed.
*/
- if (segno > entry->segno)
- continue;
- /* WAL segments must be archived in order */
- pg_log_error("WAL files are not archived in sequential order");
- pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
- segno, entry->segno);
- exit(1);
+ /* Create a temporary file if one does not already exist */
+ if (!entry->tmpseg_exists)
+ {
+ write_fp = prepare_tmp_write(entry->segno);
+ entry->tmpseg_exists = true;
+ }
+
+ /* Flush data from the buffer to the file */
+ perform_tmp_write(entry->segno, &entry->buf, write_fp);
+ resetStringInfo(&entry->buf);
+
+ /*
+ * The change in the current segment entry indicates that the reading
+ * of this file has ended.
+ */
+ if (entry != privateInfo->cur_wal && write_fp != NULL)
+ {
+ fclose(write_fp);
+ write_fp = NULL;
+ }
}
/* Requested WAL segment not found */
@@ -441,7 +476,166 @@ get_archive_wal_entry(XLogSegNo segno, XLogDumpPrivate *privateInfo)
}
/*
- * Create an astreamer that can read WAL from a tar file.
+ * Set up a temporary directory to temporarily store WAL segments.
+ */
+static void
+setup_tmpseg_dir(const char *waldir)
+{
+ char *template;
+
+ /*
+ * Use the directory specified by the TMPDIR environment variable. If it's
+ * not set, use the provided WAL directory to extract WAL file
+ * temporarily.
+ */
+ template = psprintf("%s/waldump_tmp-XXXXXX",
+ getenv("TMPDIR") ? getenv("TMPDIR") : waldir);
+ TmpWalSegDir = mkdtemp(template);
+
+ if (TmpWalSegDir == NULL)
+ pg_fatal("could not create directory \"%s\": %m", template);
+
+ canonicalize_path(TmpWalSegDir);
+
+ pg_log_debug("created directory \"%s\"", TmpWalSegDir);
+}
+
+/*
+ * Removes the temporarily store WAL segments, if any, at exiting.
+ */
+static void
+cleanup_tmpseg_dir_atexit(void)
+{
+ ArchivedWAL_iterator it;
+ ArchivedWALEntry *entry;
+
+ /* Remove temporary segments */
+ ArchivedWAL_start_iterate(ArchivedWAL_HTAB, &it);
+ while ((entry = ArchivedWAL_iterate(ArchivedWAL_HTAB, &it)) != NULL)
+ {
+ if (entry->tmpseg_exists)
+ {
+ remove_tmp_walseg(entry->segno, false);
+ entry->tmpseg_exists = false;
+ }
+ }
+
+ /* Remove temporary directory */
+ if (rmdir(TmpWalSegDir) == 0)
+ pg_log_debug("removed directory \"%s\"", TmpWalSegDir);
+}
+
+/*
+ * Generate the temporary WAL file path.
+ *
+ * Note that the caller is responsible to pfree it.
+ */
+char *
+get_tmp_walseg_path(XLogSegNo segno)
+{
+ char *fpath = (char *) palloc(MAXPGPATH);
+
+ Assert(TmpWalSegDir);
+
+ snprintf(fpath, MAXPGPATH, "%s/%08X%08X",
+ TmpWalSegDir,
+ (uint32) (segno / XLogSegmentsPerXLogId(WalSegSz)),
+ (uint32) (segno % XLogSegmentsPerXLogId(WalSegSz)));
+
+ return fpath;
+}
+
+/*
+ * Routine to check whether a temporary file exists for the corresponding WAL
+ * segment number.
+ */
+bool
+tmp_walseg_exists(XLogSegNo segno)
+{
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+
+ if (entry == NULL)
+ return false;
+
+ return entry->tmpseg_exists;
+}
+
+/*
+ * Create an empty placeholder file and return its handle.
+ */
+static FILE *
+prepare_tmp_write(XLogSegNo segno)
+{
+ FILE *file;
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(segno);
+
+ /* 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_debug("temporarily exporting file \"%s\"", fpath);
+ pfree(fpath);
+
+ return file;
+}
+
+/*
+ * Write buffer data to the given file handle.
+ */
+static void
+perform_tmp_write(XLogSegNo segno, StringInfo buf, FILE *file)
+{
+ Assert(file);
+
+ errno = 0;
+ if (buf->len > 0 && fwrite(buf->data, buf->len, 1, file) != 1)
+ {
+ /*
+ * 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_walseg_path(segno));
+ }
+}
+
+/*
+ * Remove temporary file
+ */
+void
+remove_tmp_walseg(XLogSegNo segno, bool update_entry)
+{
+ char *fpath = get_tmp_walseg_path(segno);
+
+ if (unlink(fpath) == 0)
+ pg_log_debug("removed file \"%s\"", fpath);
+ pfree(fpath);
+
+ /* Update entry if requested */
+ if (update_entry)
+ {
+ ArchivedWALEntry *entry;
+
+ entry = ArchivedWAL_lookup(ArchivedWAL_HTAB, segno);
+ Assert(entry != NULL);
+ entry->tmpseg_exists = false;
+ }
+}
+
+/*
+ * Create an astreamer that can read WAL from tar file.
*/
static astreamer *
astreamer_waldump_new(XLogDumpPrivate *privateInfo)
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index e2c96c3f4ca..96d17e99690 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -474,12 +474,51 @@ TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
{
XLogDumpPrivate *private = state->private_data;
int count = required_read_len(private, targetPagePtr, reqLen);
+ XLogSegNo nextSegNo;
/* Bail out if the count to be read is not valid */
if (count < 0)
return -1;
- /* Read the WAL page from the archive streamer */
+ /*
+ * 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))
+ {
+ 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. */
+ remove_tmp_walseg(nextSegNo, true);
+ }
+
+ 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
+ */
+ if (tmp_walseg_exists(nextSegNo))
+ {
+ char *fpath;
+
+ fpath = get_tmp_walseg_path(nextSegNo);
+ state->seg.ws_file = open(fpath, O_RDONLY | PG_BINARY, 0);
+ pfree(fpath);
+ }
+ }
+
+ /* Continue reading from the open WAL segment, if any */
+ if (state->seg.ws_file >= 0)
+ return WALDumpReadPage(state, targetPagePtr, count, targetPtr,
+ readBuff);
+
+ /* Otherwise, read the WAL page from the archive streamer */
return read_archive_wal_page(private, targetPagePtr, count, readBuff);
}
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
index ec7a33d40e0..03e02625ba1 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -58,4 +58,8 @@ extern int read_archive_wal_page(XLogDumpPrivate *privateInfo,
XLogRecPtr targetPagePtr,
Size count, char *readBuff);
+extern char *get_tmp_walseg_path(XLogSegNo segno);
+extern bool tmp_walseg_exists(XLogSegNo segno);
+extern void remove_tmp_walseg(XLogSegNo segno, bool update_entry);
+
#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 13567fbdba1..68b0cdd29e5 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -7,6 +7,7 @@ use Cwd;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+use List::Util qw(shuffle);
my $tar = $ENV{TAR};
@@ -272,7 +273,7 @@ sub generate_archive
}
closedir $dh;
- @files = sort @files;
+ @files = shuffle @files;
# move into the WAL directory before archiving files
my $cwd = getcwd;
--
2.47.1
v10-0006-pg_verifybackup-Delay-default-WAL-directory-prep.patchapplication/octet-stream; name=v10-0006-pg_verifybackup-Delay-default-WAL-directory-prep.patchDownload
From 4f62c2263fb567811dd11b275d883ea1e0b7a4db Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 14:47:43 +0530
Subject: [PATCH v10 6/8] pg_verifybackup: Delay default WAL directory
preparation.
We are not sure whether to parse WAL from a directory or an archive
until the backup format is known. Therefore, we delay preparing the
default WAL directory until the point of parsing. This delay is
harmless, as the WAL directory is not used elsewhere.
---
src/bin/pg_verifybackup/pg_verifybackup.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 456e0375e13..c6be75eba74 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -285,10 +285,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
@@ -368,6 +364,10 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
+ /* 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 parse the required ranges of WAL records, unless we were told
* not to do so.
--
2.47.1
v10-0007-pg_verifybackup-Rename-the-wal-directory-switch-.patchapplication/octet-stream; name=v10-0007-pg_verifybackup-Rename-the-wal-directory-switch-.patchDownload
From d547aeb99303cc83cb1f8ad233804eb2e603d72e Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 25 Nov 2025 17:32:14 +0530
Subject: [PATCH v10 7/8] pg_verifybackup: Rename the wal-directory switch to
wal-path
With previous patches to pg_waldump can now decode WAL directly from
tar files. This means you'll be able to specify a tar archive path
instead of a traditional WAL directory.
To keep things consistent and more versatile, we should also
generalize the input switch for pg_verifybackup. It should accept
either a directory or a tar file path that contains WALs. This change
will also aligning it with the existing manifest-path switch naming.
== NOTE ==
The corresponding PO files require updating due to this change.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 2 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 22 +++++++++++-----------
src/bin/pg_verifybackup/t/007_wal.pl | 4 ++--
3 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index 61c12975e4a..e9b8bfd51b1 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -261,7 +261,7 @@ PostgreSQL documentation
<varlistentry>
<term><option>-w <replaceable class="parameter">path</replaceable></option></term>
- <term><option>--wal-directory=<replaceable class="parameter">path</replaceable></option></term>
+ <term><option>--wal-path=<replaceable class="parameter">path</replaceable></option></term>
<listitem>
<para>
Try to parse WAL files stored in the specified directory, rather than
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index c6be75eba74..97178d585c3 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -93,7 +93,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 +126,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 +135,7 @@ 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 *pg_waldump_path = NULL;
DIR *dir;
@@ -221,8 +221,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 */
@@ -365,15 +365,15 @@ main(int argc, char **argv)
verify_backup_checksums(&context);
/* By default, look for the WAL in the backup directory, too. */
- if (wal_directory == NULL)
- wal_directory = psprintf("%s/pg_wal", context.backup_directory);
+ if (wal_path == NULL)
+ wal_path = psprintf("%s/pg_wal", context.backup_directory);
/*
* 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
@@ -1198,7 +1198,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 +1208,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 +1376,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/t/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl
index 79087a1f6be..8ad2234453d 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', '--no-parse-wal', $backup_path ],
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: $!";
--
2.47.1
v10-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-form.patchapplication/octet-stream; name=v10-0008-pg_verifybackup-enabled-WAL-parsing-for-tar-form.patchDownload
From 31791ed0320af216c80a8a72017670f0b0e06752 Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Tue, 25 Nov 2025 17:34:26 +0530
Subject: [PATCH v10 8/8] pg_verifybackup: enabled WAL parsing for tar-format
backup
Now that pg_waldump supports decoding from tar archives, we should
leverage this functionality to remove the previous restriction on WAL
parsing for tar-backed formats.
---
doc/src/sgml/ref/pg_verifybackup.sgml | 5 +-
src/bin/pg_verifybackup/pg_verifybackup.c | 66 +++++++++++++------
src/bin/pg_verifybackup/t/002_algorithm.pl | 4 --
src/bin/pg_verifybackup/t/003_corruption.pl | 4 +-
src/bin/pg_verifybackup/t/008_untar.pl | 5 +-
src/bin/pg_verifybackup/t/010_client_untar.pl | 5 +-
6 files changed, 50 insertions(+), 39 deletions(-)
diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml
index e9b8bfd51b1..16b50b5a4df 100644
--- a/doc/src/sgml/ref/pg_verifybackup.sgml
+++ b/doc/src/sgml/ref/pg_verifybackup.sgml
@@ -36,10 +36,7 @@ PostgreSQL documentation
<literal>backup_manifest</literal> 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 <application>pg_basebackup</application>. However, at present,
- <literal>WAL</literal> verification is supported only for plain-format
- backups. Therefore, if the backup is stored in tar-format, the
- <literal>-n, --no-parse-wal</literal> option should be used.
+ supported by <application>pg_basebackup</application>.
</para>
<para>
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index 97178d585c3..baa5f9dfa5e 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);
@@ -136,6 +140,8 @@ main(int argc, char **argv)
bool no_parse_wal = false;
bool quiet = false;
char *wal_path = NULL;
+ char *base_archive_path = NULL;
+ char *wal_archive_path = NULL;
char *pg_waldump_path = NULL;
DIR *dir;
@@ -327,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'.
@@ -346,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.
@@ -364,9 +359,28 @@ main(int argc, char **argv)
if (context.format == 'p' && !context.skip_checksums)
verify_backup_checksums(&context);
- /* By default, look for the WAL in the backup directory, too. */
+ /*
+ * 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)
- wal_path = psprintf("%s/pg_wal", context.backup_directory);
+ {
+ 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
@@ -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 <tablespaceoid>.tar file here. */
diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl
index 0556191ec9d..edc515d5904 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 b1d65b8aa0f..882d75d9dc2 100644
--- a/src/bin/pg_verifybackup/t/003_corruption.pl
+++ b/src/bin/pg_verifybackup/t/003_corruption.pl
@@ -193,10 +193,8 @@ for my $scenario (@scenario)
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/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl
index ae67ae85a31..161c08c190d 100644
--- a/src/bin/pg_verifybackup/t/008_untar.pl
+++ b/src/bin/pg_verifybackup/t/008_untar.pl
@@ -47,7 +47,6 @@ my $tsoid = $primary->safe_psql(
SELECT oid FROM pg_tablespace WHERE spcname = 'regress_ts1'));
my $backup_path = $primary->backup_dir . '/server-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -123,14 +122,12 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
rmtree($backup_path);
- rmtree($extract_path);
}
}
diff --git a/src/bin/pg_verifybackup/t/010_client_untar.pl b/src/bin/pg_verifybackup/t/010_client_untar.pl
index 1ac7b5db75a..9670fbe4fda 100644
--- a/src/bin/pg_verifybackup/t/010_client_untar.pl
+++ b/src/bin/pg_verifybackup/t/010_client_untar.pl
@@ -32,7 +32,6 @@ print $jf $junk_data;
close $jf;
my $backup_path = $primary->backup_dir . '/client-backup';
-my $extract_path = $primary->backup_dir . '/extracted-backup';
my @test_configuration = (
{
@@ -137,13 +136,11 @@ for my $tc (@test_configuration)
# 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");
# Cleanup.
- rmtree($extract_path);
rmtree($backup_path);
}
}
--
2.47.1