v1-0005-pg_waldump-Add-support-for-archived-WAL-decoding.patch
application/x-patch
Filename: v1-0005-pg_waldump-Add-support-for-archived-WAL-decoding.patch
Type: application/x-patch
Part: 4
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v1-0005
Subject: pg_waldump: Add support for archived WAL decoding.
| File | + | − |
|---|---|---|
| doc/src/sgml/ref/pg_waldump.sgml | 6 | 2 |
| src/bin/pg_waldump/astreamer_waldump.c | 378 | 0 |
| src/bin/pg_waldump/Makefile | 6 | 1 |
| src/bin/pg_waldump/meson.build | 3 | 1 |
| src/bin/pg_waldump/pg_waldump.c | 299 | 62 |
| src/bin/pg_waldump/pg_waldump.h | 20 | 1 |
| src/bin/pg_waldump/t/001_basic.pl | 52 | 12 |
| src/tools/pgindent/typedefs.list | 1 | 0 |
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