Thread
Commits
GET /api/v1/messages/:b64id/commits
the thread's linked commits as JSON, with link sources.
API reference →
-
Use size_t instead of Size in pg_waldump
- 3f8913f683ba 19 (unreleased) landed
-
More tar portability adjustments.
- 5079e420b92d 18.4 landed
- bab656bb87b0 19 (unreleased) landed
-
Further harden tests that might use not-so-compatible tar versions.
- ebba64c08d96 19 (unreleased) landed
- c4b7be4ecb12 18.4 landed
-
Harden astreamer tar parsing logic against archives it can't handle.
- bc30c704add5 19 (unreleased) landed
- 698eae7db7ab 18.4 landed
-
Fix pg_waldump/t/001_basic.pl with BSD tar on ZFS.
- 852de579a6ee 19 (unreleased) landed
-
Remove a low-value, high-risk optimization in pg_waldump.
- e9d723487b26 19 (unreleased) landed
-
Fix misuse of simplehash.h hash operations in pg_waldump.
- ff84efe4fdc0 19 (unreleased) landed
-
Fix file descriptor leakages in pg_waldump.
- 03b1e30e7aa5 19 (unreleased) landed
-
Fix poorly-sized buffers in astreamer compression modules.
- 6e243d81c549 19 (unreleased) landed
-
Remove read_archive_file()'s "count" parameter.
- ca1f1ade3f64 19 (unreleased) landed
-
Report detailed errors from XLogFindNextRecord() failures.
- 1c162c965a11 19 (unreleased) cited
-
Fix assorted bugs in archive_waldump.c.
- 860359ea029f 19 (unreleased) landed
-
Remove nonfunctional tar file trailer size check.
- 5868372bbfb9 19 (unreleased) landed
-
Fix finalization of decompressor astreamers.
- 9aa1fcc54702 19 (unreleased) landed
- 80785a527b9c 16.14 landed
- 6ccfc4492212 17.10 landed
- 5f9642614275 18.4 landed
- 5540f9c430f6 15.18 landed
-
Move tar detection and compression logic to common.
- c8a350a43982 19 (unreleased) landed
-
pg_verifybackup: Enable WAL parsing for tar-format backups
- b3cf461b3cf9 19 (unreleased) landed
-
pg_waldump: Add support for reading WAL from tar archives
- b15c1513984e 19 (unreleased) landed
-
pg_waldump: Preparatory refactoring for tar archive WAL decoding.
- f8a0cd267170 19 (unreleased) landed
-
pg_waldump: Remove file-level global WalSegSz.
- 9a446d0256dc 19 (unreleased) landed
-
pg_verifybackup: Verify tar-format backups.
- 8dfd31290279 18.0 cited
-
pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-08-07T14:17:08Z
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
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-08-25T12:28:44Z
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
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-08-26T11:52:20Z
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
-
Re: pg_waldump: support decoding of WAL inside tarfile
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-09-08T13:37:02Z
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. -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-09-12T10:55:28Z
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
-
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2025-09-12T18:28:15Z
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 -
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2025-09-12T20:27:07Z
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
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-09-25T08:18:13Z
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 -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-09-25T08:24:51Z
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 -
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2025-09-29T15:15:31Z
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
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-09-29T16:17:10Z
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
-
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2025-10-10T18:01:59Z
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 -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-10-16T11:48:31Z
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" 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? > 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 -
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2025-10-20T14:34:50Z
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 -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-11-06T09:03:05Z
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 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. > 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 -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-11-17T04:50:48Z
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
-
Re: pg_waldump: support decoding of WAL inside tarfile
Jakub Wartak <jakub.wartak@enterprisedb.com> — 2025-11-19T08:20:14Z
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. -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-11-21T11:44:26Z
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 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. > 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
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-11-21T12:16:24Z
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
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-11-25T06:37:32Z
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
-
Re: pg_waldump: support decoding of WAL inside tarfile
Chao Li <li.evan.chao@gmail.com> — 2025-11-25T08:50:29Z
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/ -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2025-11-26T06:02:04Z
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 -
Re: pg_waldump: support decoding of WAL inside tarfile
Chao Li <li.evan.chao@gmail.com> — 2025-11-26T07:23:39Z
> 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/ -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-01-02T12:29:54Z
Attached is the rebased version, includes some code comment improvements. Regards, Amul
-
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2026-01-16T20:38:03Z
On Fri, Jan 2, 2026 at 7:30 AM Amul Sul <sulamul@gmail.com> wrote: > Attached is the rebased version, includes some code comment improvements. Regarding 0001, I don't think that passing the WAL segment size around in a global variable is a good practice. It makes it hard to tell whether the variable is guaranteed to have been set properly at all the places where it is used. Of course, this patch set does not introduce that problem, but I think it could avoid making it worse by adding WalSegSz to XLogDumpPrivate instead of exporting the global variable everywhere. That struct seems to be available everyplace where we need the value. Actually, though, I'm inclined to remove the file-level global as well. Patch for that attached. I'm not against global variables in general, but I'm against global variables that are just there to avoid passing the right stuff around via function argument lists. 0002 claims to be just a refactoring but if (unlikely(private->endptr_reached)) return -1 is net new code, so I object. I realize this contradicts the review comment from Chao Li, but the principal that refactoring shouldn't change the logic is more important than whatever value we might get out of adding this sanity check here -- and I bet if we look into it we'll find that there are lots of other places in PostgreSQL that would also need to be changed if we wanted such a sanity check everywhere that we have code like this. I'd also like to point out that the commit message isn't really adequate to understand why this helps achieve the overall goal of the patch set. 0003 looks functionally correct but notice that you've stolen some but not all of the tests from the "# various ways of specifying WAL range" section without copying the comment or explaining in the commit message why you've moved some tests but not others. Regarding 0004: Instead of making ArchivedWAL_HTAB a global variable, can we just store a pointer to it in XLogDumpPrivate? (I would change the name to something else, something all lower case.) Or maybe the pointer should go someplace else, but I'm skeptical of a global variable here for the same reasons as 0001. I think ArchivedWALFile would be a better name than ArchivedWALEntry, and cur_wal_file or cur_file would be a better name than cur_wal. I'm extremely happy with the way that the hash table looks and the new logic in init_archive_reader() to make sure that we don't need to reread the beginning of the WAL but can reuse the data already read. That seems like a huge improvement to me. + 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); + } + } It feels pretty inelegant to me to pull data back into &entry->buf from readBuff here. I think what you should do is just do this test once, before entering the while (nbytes > 0) loop. Then you'll always have p == readBuff, so the nested if statement is no longer required. Also, the way you have it, if this triggers for the first loop iteration, it will trigger for every subsquent loop iteration, which seems wasteful. + 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; + } This logic seems pretty confusing. It looks as though len can go negative, because what if skipBytes > len? I'm not sure that's really possible here, but there's no comments explaining why it can't and no assertion verifying that it doesn't. I think you should probably try to unify this with the following if statement that is gated by (len > 0). In other places where we have code like this, what I've usually found useful is to compute the number of bytes to copy as Min(bytes available from source, bytes that will fit into the target), and I think that technique might be useful here. For example, in BlockRefTableRead, length is always the number of bytes that the caller wants which we haven't yet read, and we compute bytes_to_copy = Min(length, buffer->used - buffer->cursor). We then memcpy(data, &buffer->data[buffer->cursor], bytes_to_copy) and then adjust data and length. A similar bit of code not written by me can be found in WALReadFromBuffers, which maintains nbytes as the remaining number of bytes to be copied and then computes the size of the next copy as npagebytes = Min(nbytes, XLOG_BLCKSZ - offset). I think your if (len > 0 && recptr > startPtr) block is trying to do something similar to what the Min() is doing in those examples, but I find it hard to understand whether it is doing it correctly. I think you need some combination of clearer variable names, more comments, and/or rewriting this to be easier to read and understand without help. + entry = privateInfo->cur_wal; + + /* Fetch more data */ + if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0) + break; /* archive file ended */ Why do we set entry to privateInfo->cur_wal and then call read_archive_file() afterwards? Doesn't read_archive_file() have the potential to change cur_wal? If so, don't we want to look at the entry that is current after we've called read_archive_file(), rather than before? Also, to harp on the naming a bit more, stuff like entry = privateInfo->cur_wal kind of shows that you haven't made the naming real consistent. This could be more clear if it said something like wal_file = privateInfo->cur_wal_file. + /* 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; + } + I don't really understand what's happening here. One thing that's odd is that the first of these two if statements considers the timeline and the second does not. It seems unlikely that this is correct. I suspect that the hash table needs to be keyed by (TLI, segno) rather than just by TLI, and that get_archive_wal_entry needs to take both a TLI and a segment number as an argument. Otherwise, what could possibly prevent the first if statement from returning the correct segment from the wrong timeline? (Likewise, I think stuff in 0005 like prepare_tmp_write() should work on a TLI and a segno, not just a segno. There probably should be very few places in the patch where you pass only a segno and no TLI. get_tmp_walseg_path() is a pretty clear sign of the problem: If you were including the TLI in the file name, you would just use XLogFileName() here, but because you left out the TLI, you end up having to invent your own file naming convention. That seems pretty bad: wouldn't we like the temporary file names to have the same names as the files inside the archive? What happens if the same WAL file exists in the archive with two different TLIs?) Another thing that's odd is: why is this function resetting privateInfo->cur_wal to NULL? I feel like it should be the job of astreamer_waldump_content() to manage the value of privateInfo->cur_wal, and this function doesn't seem to have any business touching it. Even if it thinks that we want to ignore that WAL file for purposes of *this* call to get_archive_wal_entry(), surely it has no right to decide that the *next* call to this function should ignore that entry as well. Honestly, I don't really understand why we need to poke into privateInfo at all here. I feel like the shape of this loop should be: while (1) { if (read_archive_file(...) == 0) break; if (we have the correct entry) return entry; } }. It seems to me that it ought to be the job of the archive streamer to handle everything else, including buffering more data and spilling to files. This should just iterate, repeatedly calling read_archive_file(), until we either get to the file we want or run out of archive. + /* + * 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; I couldn't figure out what was going on here for a while. Then I realized that this makes some sense: if (segno > entry->segno), that means that the segment number that we've found in the archive is older than the requested segment number. Because we don't back up, that means we don't need the data. I think the idea is that we would fall through here and buffer the data if this if-test fails, but patch 0005 actually removes these two lines of code, so I think something here is kind of messed up. I think the result will be that with both patches applied, we'll buffer data we don't actually need for anything. Generally, I think that some details here should be revisited: +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; The first few members are great, except that timeline needs to be part of the hash key as well, so you probably need to make a two-element struct with a segno and a TLI and make that struct be the thing that comes right after status. After that, I'm not so sure. Essentially, I think what's happening here is that segno*file_size+total_read is the end LSN for buf, and the start LSN value is that value minus buf.len. And if tmpseg_exists is true, then the buffer might be empty, and you can read however much of the segment we found from the temp file. I find that confusing. If we're going to do something like this, I feel like it would make more sense to store XLogRecPtr starttli instead of int total_read, so that we explicitly mention the first LSN stored in the buffer, and you infer the ending LSN from the length of the buffer. But, stepping back a bit, why do we have a StringInfoData in every ArchivedWALEntry? It makes sense to do it that way if we're going to buffer all of the WAL data in memory. In that case, you need a separate buffer for every ArchivedWALEntry, and so this is logical. But if we're going to spill to disk, then there should only ever be one buffer in use at any given time, so why have a buffer in every ArchivedWALEntry instead of a single buffer inside of XLogDumpPrivate? We could choose to treat the hash table as the set of previous segments for which we have written data to disk, and the current segment and any associated buffer would be tracked inside the XLogDumpPrivate or astreamer_waldump, which would mean that only one copy of them would exist. I'm tempted to say that the direction I'm sketching here is the right way to go, but I'm not 100% certain that's true. It could be write to have a buffer per WAL file, as you have here, but if so, we should know why we're doing that and how it fits into the design, and I'm not currently seeing a real reason for it. + if (strstr(member->pathname, "PaxHeaders.")) + return false; There is no way that a filename containing the string "PaxHeaders." could ever pass the IsXLogFileName test just above. We shouldn't need this. -- Robert Haas EDB: http://www.enterprisedb.com -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-01-19T11:13:30Z
On Sat, Jan 17, 2026 at 2:08 AM Robert Haas <robertmhaas@gmail.com> wrote: > > On Fri, Jan 2, 2026 at 7:30 AM Amul Sul <sulamul@gmail.com> wrote: > > Attached is the rebased version, includes some code comment improvements. > Thanks for the feedback. I am replying to a few comments inline below regarding elements that are unlikely to change -- explaining the reasoning why I believe they are correct -- just to ensure we are aligned. For the remaining points, I generally agree and will either implement the improvements or provide my reasoning if I don't. > > + 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; > + } > > This logic seems pretty confusing. It looks as though len can go > negative, because what if skipBytes > len? I'm not sure that's really > possible here, but there's no comments explaining why it can't and no > assertion verifying that it doesn't. I think you should probably try > to unify this with the following if statement that is gated by (len > > 0). The preceding check if (recptr >= endPtr) ensures that within this block, recptr is always less than endPtr. Since startPtr is derived as endPtr - len, recptr naturally falls within the expected range (i.e., recptr - startPtr <= len). However, I agree that we can simplify this by combining it with the subsequent if statement and/or by adding an assert there. > > + entry = privateInfo->cur_wal; > + > + /* Fetch more data */ > + if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0) > + break; /* archive file ended */ > > Why do we set entry to privateInfo->cur_wal and then call > read_archive_file() afterwards? Doesn't read_archive_file() have the > potential to change cur_wal? If so, don't we want to look at the entry > that is current after we've called read_archive_file(), rather than > before? Also, to harp on the naming a bit more, stuff like entry = > privateInfo->cur_wal kind of shows that you haven't made the naming > real consistent. This could be more clear if it said something like > wal_file = privateInfo->cur_wal_file. I see your point, and you’re right that assigning the entry to privateInfo->cur_wal here seems useless; however, it becomes necessary for the logic in patch 0005. I assigned it just before read_archive_file() because that function can change where the current WAL ends and a new one starts. I need to track the completed WAL entry at that moment, as it may contain data that needs to be spilled to a temporary file. I'll add a comment to clarify this. I also agree with your feedback on variable naming and will improve that in the next revision. > > + /* 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; > + } > + > > I don't really understand what's happening here. One thing that's odd > is that the first of these two if statements considers the timeline > and the second does not. It seems unlikely that this is correct. I > suspect that the hash table needs to be keyed by (TLI, segno) rather > than just by TLI, and that get_archive_wal_entry needs to take both a > TLI and a segment number as an argument. Otherwise, what could > possibly prevent the first if statement from returning the correct > segment from the wrong timeline? Yeah, I need to swap the positions of these checks. > > Another thing that's odd is: why is this function resetting > privateInfo->cur_wal to NULL? I feel like it should be the job of > astreamer_waldump_content() to manage the value of > privateInfo->cur_wal, and this function doesn't seem to have any > business touching it. Even if it thinks that we want to ignore that > WAL file for purposes of *this* call to get_archive_wal_entry(), > surely it has no right to decide that the *next* call to this function > should ignore that entry as well. > > Honestly, I don't really understand why we need to poke into > privateInfo at all here. I feel like the shape of this loop should be: > while (1) { if (read_archive_file(...) == 0) break; if (we have the > correct entry) return entry; } }. It seems to me that it ought to be > the job of the archive streamer to handle everything else, including > buffering more data and spilling to files. This should just iterate, > repeatedly calling read_archive_file(), until we either get to the > file we want or run out of archive. > We previously tried having the astreamer function perform all necessary checks (e.g., TLI and segment number), but this became problematic in init_archive_reader when we needed to read a single WAL page just to determine the WAL segment size without any validation. To bypass those checks, I initially used a READ_ANY_WAL() macro, but it felt like a hack. In the current design, the archive streamer is responsible only for reading and copying data to the buffer. To optimize this and avoid unnecessary memcpy and data buffering for WAL files that aren't needed, we set privateInfo->cur_wal to NULL, signaling the streamer code to skip those operations. But, I am thinking that instead of setting privateInfo->cur_wal to NULL, we could simply discard the buffer data at that place and let memcpy in astreamer_content copy if it would be that much of an issue. > + /* > + * 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; > > I couldn't figure out what was going on here for a while. Then I > realized that this makes some sense: if (segno > entry->segno), that > means that the segment number that we've found in the archive is older > than the requested segment number. Because we don't back up, that > means we don't need the data. I think the idea is that we would fall > through here and buffer the data if this if-test fails, but patch 0005 > actually removes these two lines of code, so I think something here is > kind of messed up. I think the result will be that with both patches > applied, we'll buffer data we don't actually need for anything. > Yeah, should discard buffered data too. > Generally, I think that some details here should be revisited: > > +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; > > The first few members are great, except that timeline needs to be part > of the hash key as well, so you probably need to make a two-element > struct with a segno and a TLI and make that struct be the thing that > comes right after status. After that, I'm not so sure. Essentially, I > think what's happening here is that segno*file_size+total_read is the > end LSN for buf, and the start LSN value is that value minus buf.len. > And if tmpseg_exists is true, then the buffer might be empty, and you > can read however much of the segment we found from the temp file. I > find that confusing. If we're going to do something like this, I feel > like it would make more sense to store XLogRecPtr starttli instead of > int total_read, so that we explicitly mention the first LSN stored in > the buffer, and you infer the ending LSN from the length of the > buffer. > total_read is not only the total of buffered data; it also includes data that has been spilled to a temporary file. If the buffer is not empty, the most recently read data from the archive exists there; otherwise, if a temporary file exists, that data will be found there. In short, it tracks the total bytes of WAL that have been read from the archive. I will improve the code comment to reflect this. > But, stepping back a bit, why do we have a StringInfoData in every > ArchivedWALEntry? It makes sense to do it that way if we're going to > buffer all of the WAL data in memory. In that case, you need a > separate buffer for every ArchivedWALEntry, and so this is logical. > But if we're going to spill to disk, then there should only ever be > one buffer in use at any given time, so why have a buffer in every > ArchivedWALEntry instead of a single buffer inside of XLogDumpPrivate? > We could choose to treat the hash table as the set of previous > segments for which we have written data to disk, and the current > segment and any associated buffer would be tracked inside the > XLogDumpPrivate or astreamer_waldump, which would mean that only one > copy of them would exist. I'm tempted to say that the direction I'm > sketching here is the right way to go, but I'm not 100% certain that's > true. It could be write to have a buffer per WAL file, as you have > here, but if so, we should know why we're doing that and how it fits > into the design, and I'm not currently seeing a real reason for it. > In the earlier version, we had only one buffer where we copied streamed data, but a problem (previously discussed [1]) arose when the archive streamer reads data from two WAL files -- i.e., one is ending and the next is starting. We have to keep track of where the previous file ends and the next one starts, along with the WAL segment information that each belongs to. > + if (strstr(member->pathname, "PaxHeaders.")) > + return false; > > There is no way that a filename containing the string "PaxHeaders." > could ever pass the IsXLogFileName test just above. We shouldn't need > this. > This checks the directory name (e.g., x_dir/y_dir/PaxHeaders.NNNN/wal_file_name). The name of that metadata file is exactly the same as the WAL file name, which is why IsXLogFileName() doesn't help here. Regards, Amul 1] http://postgr.es/m/CA+Tgmoardk4VuthHc23vov+AVkhq7eT0mFUs-2ctAnP1uiTaog@mail.gmail.com -
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2026-01-19T13:30:48Z
On Mon, Jan 19, 2026 at 6:14 AM Amul Sul <sulamul@gmail.com> wrote: > We previously tried having the astreamer function perform all > necessary checks (e.g., TLI and segment number), but this became > problematic in init_archive_reader when we needed to read a single WAL > page just to determine the WAL segment size without any validation. To > bypass those checks, I initially used a READ_ANY_WAL() macro, but it > felt like a hack. > > In the current design, the archive streamer is responsible only for > reading and copying data to the buffer. To optimize this and avoid > unnecessary memcpy and data buffering for WAL files that aren't > needed, we set privateInfo->cur_wal to NULL, signaling the streamer > code to skip those operations. Hmm. In that case, the code definitely deserves a comment, as that was not at all clear to me. > But, I am thinking that instead of setting privateInfo->cur_wal to > NULL, we could simply discard the buffer data at that place and let > memcpy in astreamer_content copy if it would be that much of an issue. I don't think doing unnecessary copying is the right way forward. The copying itself could be expensive, but I am a little concerned about the memory utilization of this code. Suppose the user has increased the WAL segment size to 1GB or even higher. It seems like we could buffer a whole segment or maybe even more in some scenarios. If we avoid copying data we don't need, then we also avoid buffering it in memory. In terms of the separation of concerns, we could view setting privateInfo->cur_wal = NULL as a form of signaling, a way for this code to tell the astreamer that it doesn't need the data buffering. However, I think it might be better to make the signaling more explicit. Instead of having the caller directly set the buffer to NULL, or directly trim data out of the buffer, maybe it should set some value in privateInfo that tells the astreamer what to do. For instance, suppose it sets the oldest LSN that it might still care about in privateInfo, and then the astreamer is free to do skip copying of any data prior to that LSN, and discard any that it already has. Especially if properly commented, I think this might be more clear than what you have now. I am not 100% sure that's the right idea, though. I just think right now it's a bit murky who does what and why the division of responsibilities is what it is. For example, read_archive_wal_page() currently has some responsibility for discarding data, but is that because it is the right place to discard data, or is that just because that's where we have an LSN available that tells us what we can discard? I don't know, but an explicit signaling mechanism like what I describe in the previous paragraph can make it possible for those two things to happen in different places. > In the earlier version, we had only one buffer where we copied > streamed data, but a problem (previously discussed [1]) arose when the > archive streamer reads data from two WAL files -- i.e., one is ending > and the next is starting. We have to keep track of where the previous > file ends and the next one starts, along with the WAL segment > information that each belongs to. Hmm, interesting. My first thought was that if we were starting a new WAL file, isn't it time to flush the data from the old one to a spill file? But maybe it's not, if the WAL reader can reread records, and a record can span file boundaries. But probably we need some comment so that the next reader is not confused, and we definitely need to make sure that the structure members in ArchivedWALEntry are well-chosen and well-documented. > > + if (strstr(member->pathname, "PaxHeaders.")) > > + return false; > > > > There is no way that a filename containing the string "PaxHeaders." > > could ever pass the IsXLogFileName test just above. We shouldn't need > > this. > > > > This checks the directory name (e.g., > x_dir/y_dir/PaxHeaders.NNNN/wal_file_name). The name of that metadata > file is exactly the same as the WAL file name, which is why > IsXLogFileName() doesn't help here. I think this code should only be considering files in the toplevel directory, and skipping over any directories it finds. I absolutely promise I am not going to commit anything that is specifically looking for PaxHeaders. Nothing we've ever done with tar files up to now has required that, and I don't think this should, either. -- Robert Haas EDB: http://www.enterprisedb.com
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-01-23T12:27:11Z
On Mon, Jan 19, 2026 at 7:01 PM Robert Haas <robertmhaas@gmail.com> wrote: > > > But, I am thinking that instead of setting privateInfo->cur_wal to > > NULL, we could simply discard the buffer data at that place and let > > memcpy in astreamer_content copy if it would be that much of an issue. > > I don't think doing unnecessary copying is the right way forward. The > copying itself could be expensive, but I am a little concerned about > the memory utilization of this code. Suppose the user has increased > the WAL segment size to 1GB or even higher. It seems like we could > buffer a whole segment or maybe even more in some scenarios. If we > avoid copying data we don't need, then we also avoid buffering it in > memory. > > In terms of the separation of concerns, we could view setting > privateInfo->cur_wal = NULL as a form of signaling, a way for this > code to tell the astreamer that it doesn't need the data buffering. > However, I think it might be better to make the signaling more > explicit. Instead of having the caller directly set the buffer to > NULL, or directly trim data out of the buffer, maybe it should set > some value in privateInfo that tells the astreamer what to do. For > instance, suppose it sets the oldest LSN that it might still care > about in privateInfo, and then the astreamer is free to do skip > copying of any data prior to that LSN, and discard any that it already > has. Especially if properly commented, I think this might be more > clear than what you have now. I am not 100% sure that's the right > idea, though. I just think right now it's a bit murky who does what > and why the division of responsibilities is what it is. > The current implementation of astreamer_waldump_content() does not have sufficient information to skip WAL segments during the initial hash entry preparation and data-copying phase. Because the filtration parameters -- which determine if a segment should be skipped -- depend on the WAL segment size, we must first read a WAL page through the streamer to calculate that size, which is done in init_archive_reader(). Therefore, the responsibility of the archive streamer is strictly to copy the WAL segment data to the buffer. The skipping decision is handled inside get_archive_wal_entry(), which sets privateInfo->cur_wal to NULL. In the next version, I am planning to add a separate routine (with better commenting) that, along with setting the pointer to NULL, releases that hash entry to avoid unnecessary memory usage. Another option I previously considered was adding the filtration logic inside the archive streamer itself. However, since the very first read is required to calculate the WAL segment size, the filter check cannot be performed immediately. However, we could send a signal to the archive streamer via privateInfo (e.g., a read_any_wal or skip_wal_check boolean flag) to disable the filtration check until the size is calculated. But that approach isn't very elegant; if the first WAL page we read belongs to a segment we actually want to skip, we would still have to run the filter check and handle the skip/removal logic outside of the streamer (i.e., inside init_archive_reader()). This would result in performing the same filtration check in two different places. Therefore, I believe performing the filtration check through get_archive_wal_entry() and then calling a routine to clear privateInfo->cur_wal and the hash entry is the better approach, IMO. Additionally, once we consume a WAL file and move to the next one, the hash entry and buffer for that WAL can be released to prevent unnecessary memory consumption by calling the same routine that I am planning to add. > > > + if (strstr(member->pathname, "PaxHeaders.")) > > > + return false; > > > > > > There is no way that a filename containing the string "PaxHeaders." > > > could ever pass the IsXLogFileName test just above. We shouldn't need > > > this. > > > > > > > This checks the directory name (e.g., > > x_dir/y_dir/PaxHeaders.NNNN/wal_file_name). The name of that metadata > > file is exactly the same as the WAL file name, which is why > > IsXLogFileName() doesn't help here. > > I think this code should only be considering files in the toplevel > directory, and skipping over any directories it finds. I absolutely > promise I am not going to commit anything that is specifically looking > for PaxHeaders. Nothing we've ever done with tar files up to now has > required that, and I don't think this should, either. Ok, fair enough, my intention was to allow decoding of valid WAL data from any directory in the tar archive, but I will go ahead and add that restriction as suggested. Regards, Amul
-
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2026-01-26T17:22:33Z
On Fri, Jan 23, 2026 at 7:27 AM Amul Sul <sulamul@gmail.com> wrote: > Another option I previously considered was adding the filtration logic > inside the archive streamer itself. However, since the very first read > is required to calculate the WAL segment size, the filter check cannot > be performed immediately. However, we could send a signal to the > archive streamer via privateInfo (e.g., a read_any_wal or > skip_wal_check boolean flag) to disable the filtration check until the > size is calculated. But that approach isn't very elegant; if the first > WAL page we read belongs to a segment we actually want to skip, we > would still have to run the filter check and handle the skip/removal > logic outside of the streamer (i.e., inside init_archive_reader()). > This would result in performing the same filtration check in two > different places. I mean, I don't really buy this logic. If the information added to privateInfo is "here's the LSN before which you can remove stuff," and it starts out initialized to 0/0, then the read of the first WAL page causes no problem at all, because nothing is before 0/0. After it gets updated to some non-zero value, the next call to astreamer_waldump_content() can handle discarding any data we don't need. IMHO, the best argument for keeping things are is that in some cases, that decision might result in a bit of delay in discarding old data, but I don't think that really matters. I think all that we care about is the peak memory utilization of an operation, and I don't think that an explicit signaling system should increase that at all. That said, I'm certainly willing to consider other ideas about how this can work. However, I feel strongly that the logic needs to be not only correct, but clear and well-explained. Setting cur_wal to NULL to make the astreamer skip without adequate comments doesn't meet that standard. Maybe with some better comments it's all right, but frankly I'm a bit skeptical. Right now, you're using whether or not cur_wal is NULL as a signal to skip data or not skip data. How is that better than passing down the LSN and TLI that you want to read next and letting the astreamer figure out what to do itself? It's a signaling mechanism either way, but it seems a lot easier to figure out whether we always keep the LSN and TLI updated properly than to figure out whether cur_wal is always NULL at exactly the right times. -- Robert Haas EDB: http://www.enterprisedb.com
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-01-27T12:06:34Z
On Mon, Jan 26, 2026 at 10:52 PM Robert Haas <robertmhaas@gmail.com> wrote: > > On Fri, Jan 23, 2026 at 7:27 AM Amul Sul <sulamul@gmail.com> wrote: > > Another option I previously considered was adding the filtration logic > > inside the archive streamer itself. However, since the very first read > > is required to calculate the WAL segment size, the filter check cannot > > be performed immediately. However, we could send a signal to the > > archive streamer via privateInfo (e.g., a read_any_wal or > > skip_wal_check boolean flag) to disable the filtration check until the > > size is calculated. But that approach isn't very elegant; if the first > > WAL page we read belongs to a segment we actually want to skip, we > > would still have to run the filter check and handle the skip/removal > > logic outside of the streamer (i.e., inside init_archive_reader()). > > This would result in performing the same filtration check in two > > different places. > > I mean, I don't really buy this logic. If the information added to > privateInfo is "here's the LSN before which you can remove stuff," and > it starts out initialized to 0/0, then the read of the first WAL page > causes no problem at all, because nothing is before 0/0. After it gets > updated to some non-zero value, the next call to > astreamer_waldump_content() can handle discarding any data we don't > need. > > IMHO, the best argument for keeping things are is that in some cases, > that decision might result in a bit of delay in discarding old data, > but I don't think that really matters. I think all that we care about > is the peak memory utilization of an operation, and I don't think that > an explicit signaling system should increase that at all. > > That said, I'm certainly willing to consider other ideas about how > this can work. However, I feel strongly that the logic needs to be not > only correct, but clear and well-explained. Setting cur_wal to NULL to > make the astreamer skip without adequate comments doesn't meet that > standard. Maybe with some better comments it's all right, but frankly > I'm a bit skeptical. Right now, you're using whether or not cur_wal is > NULL as a signal to skip data or not skip data. How is that better > than passing down the LSN and TLI that you want to read next and > letting the astreamer figure out what to do itself? It's a signaling > mechanism either way, but it seems a lot easier to figure out whether > we always keep the LSN and TLI updated properly than to figure out > whether cur_wal is always NULL at exactly the right times. > I am not sure how to calculate the LSN of the very first page it reads at this point, since we don't have the segment size yet. Furthermore, the previous implementation using the segment number as the hash key was incorrect, as extracting the segment number and TLI is impossible without knowing the segment size, IIUC. In the attached version, I am using the WAL segment name as the hash key, which is much more straightforward. I have rewritten read_archive_wal_page(), and it looks much cleaner than before. The logic to discard irrelevant WAL files is still within get_archive_wal_entry. I added an explanation for setting cur_wal to NULL, which is now handled in the separate function I mentioned previously. Kindly have a look at the attached version; let me know if you are still not happy with the current approach for filtering/discarding irrelevant WAL segments. It isn't much different from the previous version, but I have tried to keep it in a separate routine for better code readability, with comments to make it easier to understand. I also added a comment for ArchivedWALFile. I have included your patch (removing WalSegSz) to the attached patch set as 0001; all my subsequent patches are now bumped by one. Regards, Amul
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-01-27T12:23:19Z
On Sat, Jan 17, 2026 at 2:08 AM Robert Haas <robertmhaas@gmail.com> wrote: > > On Fri, Jan 2, 2026 at 7:30 AM Amul Sul <sulamul@gmail.com> wrote: > > Attached is the rebased version, includes some code comment improvements. > Here are my replies to the remaining review comments. An updated version of the patch was posted in the previous email [1]. > Regarding 0001, I don't think that passing the WAL segment size around > in a global variable is a good practice. It makes it hard to tell > whether the variable is guaranteed to have been set properly at all > the places where it is used. Of course, this patch set does not > introduce that problem, but I think it could avoid making it worse by > adding WalSegSz to XLogDumpPrivate instead of exporting the global > variable everywhere. That struct seems to be available everyplace > where we need the value. Actually, though, I'm inclined to remove the > file-level global as well. Patch for that attached. I'm not against > global variables in general, but I'm against global variables that are > just there to avoid passing the right stuff around via function > argument lists. > Yes, it looks good. Added to the v11 patch set. > 0002 claims to be just a refactoring but if > (unlikely(private->endptr_reached)) return -1 is net new code, so I > object. I realize this contradicts the review comment from Chao Li, > but the principal that refactoring shouldn't change the logic is more > important than whatever value we might get out of adding this sanity > check here -- and I bet if we look into it we'll find that there are > lots of other places in PostgreSQL that would also need to be changed > if we wanted such a sanity check everywhere that we have code like > this. I'd also like to point out that the commit message isn't really > adequate to understand why this helps achieve the overall goal of the > patch set. > Understood, removed it. > 0003 looks functionally correct but notice that you've stolen some but > not all of the tests from the "# various ways of specifying WAL range" > section without copying the comment or explaining in the commit > message why you've moved some tests but not others. > The excluded tests provide WAL files as input to pg_waldump, which is incompatible with tar archive input. I have updated the commit message to reflect this. > Regarding 0004: > > Instead of making ArchivedWAL_HTAB a global variable, can we just > store a pointer to it in XLogDumpPrivate? (I would change the name to > something else, something all lower case.) Or maybe the pointer should > go someplace else, but I'm skeptical of a global variable here for the > same reasons as 0001. > Done. > I think ArchivedWALFile would be a better name than ArchivedWALEntry, > and cur_wal_file or cur_file would be a better name than cur_wal. > Done. > I'm extremely happy with the way that the hash table looks and the new > logic in init_archive_reader() to make sure that we don't need to > reread the beginning of the WAL but can reuse the data already read. > That seems like a huge improvement to me. > > + 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); > + } > + } > > It feels pretty inelegant to me to pull data back into &entry->buf > from readBuff here. I think what you should do is just do this test > once, before entering the while (nbytes > 0) loop. Then you'll always > have p == readBuff, so the nested if statement is no longer required. > Also, the way you have it, if this triggers for the first loop > iteration, it will trigger for every subsquent loop iteration, which > seems wasteful. > I'm not sure I follow the logic. How would this trigger a copy from readBuff back into &entry->buf on every iteration? I believe that this is a rare case: it should only happen when we reach the end of the buffer with less than the requested 8kB available. In that scenario, we copy the remaining bytes, reset the buffer, and then -- since (p > readBuff) -- perform the pull-back before invoking the archive stream for the next segment. Am I missing something in the loop condition? I replied to the rest of the review comments previously, but I’m not sure whether replying in multiple emails is the correct approach. Perhaps, since the patch is taking a bit longer, I wanted to keep the conversation moving. Regards, Amul 1] http://postgr.es/m/CAAJ_b97mWgAkj7CqdRsof4N_J-gxqjKh79b0Aw3JRo0PoZZCJg@mail.gmail.com -
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2026-01-27T15:55:23Z
On Tue, Jan 27, 2026 at 7:23 AM Amul Sul <sulamul@gmail.com> wrote: > Yes, it looks good. Added to the v11 patch set. I'll respond to the rest of this later, but I went ahead and committed that one. -- Robert Haas EDB: http://www.enterprisedb.com
-
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2026-01-27T21:11:26Z
On Tue, Jan 27, 2026 at 7:07 AM Amul Sul <sulamul@gmail.com> wrote: > In the attached version, I am using the WAL segment name as the hash > key, which is much more straightforward. I have rewritten > read_archive_wal_page(), and it looks much cleaner than before. The > logic to discard irrelevant WAL files is still within > get_archive_wal_entry. I added an explanation for setting cur_wal to > NULL, which is now handled in the separate function I mentioned > previously. > > Kindly have a look at the attached version; let me know if you are > still not happy with the current approach for filtering/discarding > irrelevant WAL segments. It isn't much different from the previous > version, but I have tried to keep it in a separate routine for better > code readability, with comments to make it easier to understand. I > also added a comment for ArchivedWALFile. I feel like the division of labor between get_archive_wal_entry() and read_archive_wal_page() is odd. I noticed this in the last version, too, and it still seems to be the case. get_archive_wal_entry() first calls ArchivedWAL_lookup(). If that finds an entry, it just returns. If it doesn't, it loops until an entry for the requested file shows up and then returns it. Then control returns to read_archive_wal_page() which loops some more until we have all the data we need for the requested file. But it seems odd to me to have two separate loops here. I think that the first loop is going to call read_archive_file() until we find the beginning of the file that we care about and then the second one is going to call read_archive_file() some more until we have read enough of it to satisfy the request. It feels odd to me to do it that way, as if we told somebody to first wait until 9 o'clock and then wait another 30 minutes, instead of just telling them to wait until 9:30. I realize it's not quite the same thing, because apart from calling read_archive_file(), the two loops do different things, but I still think it looks odd. + /* + * Ignore if the timeline is different or the current segment is not + * the desired one. + */ + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); + if (privateInfo->timeline != curSegTimeline || + privateInfo->startSegNo > curSegNo || + privateInfo->endSegNo < curSegNo || + segno > curSegNo) + { + free_archive_wal_entry(entry->fname, privateInfo); + continue; + } The comment doesn't match the code. If it did, the test would be (privateInfo->timeline != curSegTimeline || segno != curSegno). But instead the segno test is > rather than !=, and the checks against startSegNo and endSegNo aren't explained at all. I think I understand why the segno test uses > rather than !=, but it's the point of the comment to explain things like that, rather than leaving the reader to guess. And I don't know why we also need to test startSegNo and endSegNo. I also wonder what the point is of doing XLogFromFileName() on the fname provided by the caller and then again on entry->fname. Couldn't you just compare the strings? Again, the division of labor is really odd here. It's the job of astreamer_waldump_content() to skip things that aren't WAL files at all, but it's the job of get_archive_wal_entry() to skip things that are WAL files but not the one we want. I disagree with putting those checks in completely separate parts of the code. +static bool +member_is_wal_file(astreamer_waldump *mystreamer, astreamer_member *member, + char **fname) +{ + int pathlen; + char *filename; + + /* 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 files from the top-level or pg_wal directory will be decoded */ + if (pathlen > XLOG_FNAME_LEN && + strncmp(member->pathname, XLOGDIR, strlen(XLOGDIR)) != 0) + return false; + + /* WAL file could be with full path */ + filename = member->pathname + (pathlen - XLOG_FNAME_LEN); + if (!IsXLogFileName(filename)) + return false; + + *fname = pnstrdup(filename, XLOG_FNAME_LEN); + + return true; +} I don't think this code is fully correct. Note that pg_verifybackup's member_verify_header() prepends "./" and then applies canonicalize_path(). I think the same considerations apply here. Consider: [rhaas pgdata]$ (cd pg_wal; tar -cf ../pg_wal1.tar 00*) [rhaas pgdata]$ (cd pg_wal; tar -cf ../pg_wal2.tar .) [rhaas pgdata]$ tar tf pg_wal1.tar 00000001000000000000002C 00000001000000000000002D 00000001000000000000002E 00000001000000000000002F 000000010000000000000030 000000010000000000000031 000000010000000000000032 000000010000000000000033 [rhaas pgdata]$ tar tf pg_wal2.tar ./ ./00000001000000000000002C ./00000001000000000000002D ./00000001000000000000002E ./archive_status/ ./000000010000000000000031 ./000000010000000000000030 ./summaries/ ./00000001000000000000002F ./000000010000000000000032 ./000000010000000000000033 -- Robert Haas EDB: http://www.enterprisedb.com -
Re: pg_waldump: support decoding of WAL inside tarfile
Euler Taveira <euler@eulerto.com> — 2026-01-28T03:01:39Z
On Tue, Jan 27, 2026, at 9:06 AM, Amul Sul wrote: > > I have included your patch (removing WalSegSz) to the attached patch > set as 0001; all my subsequent patches are now bumped by one. > I started looking at this patch. This is not a complete review. 0002. LGTM. 0003. If this is a refactor, you should move the unlikely logic (if necessary) to 0005. 0004. You mentioned in the commit message that this patch should be merged into 0005. Reviewing it is a bit hard if you just looked at the patch. However, it is clear if you use 'git show -w $HASH0004'. I would expect that this patch contains +my $tar = $ENV{TAR}; + ... + skip "tar command is not available", 3 + if !defined $tar; because you add the SKIP. As a refactor I expect it to work independently. 0005. + pg_log_error("unnecessary command-line arguments specified with tar archive (first is \"%s\")", + argv[optind]); Which command-line arguments? This message isn't clear. How about using "option %s cannot be used together with tar archive". - 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)) Maybe I'm confused about the fact that the function name is is_archive_file() and there is a variable called is_archive. Couldn't you test private.archive_name != NULL instead of using is_archive variable? - if (!verify_directory(waldir)) + else if (!verify_directory(walpath)) { pg_log_error("could not open directory \"%s\": %m", waldir); goto bad_argument; This is not a problem of this patch but I just want to point out that it should be pg_fatal() just like similar code path a few lines below. + /* set segno to endsegno for check of --end */ + segno = endsegno; + } + + + if (!XLByteInSeg(private.endptr, segno, WalSegSz) && + private.endptr != (segno + 1) * WalSegSz) 2 blank lines. Remove one. + * archive_waldump.c + * A generic facility for reading WAL data from tar archives via archive + * streamer. The other tools (pg_basebackup and pg_verifybackup) that also use astreamer API named this similar file as astreamer_SOMETHING.c. It seems a good idea to follow the same pattern, no? Maybe astreamer_tar_archive.c or astreamer_archive.c. +/* 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; s/wal file/WAL file/ What buf is for? It is the only member that doesn't have a description. I think total_read gives the impression that you've read the file and that's the number of bytes it got. That's not true; it represents the accumulative length. Maybe read_len is a good candidate. +bool +is_archive_file(const char *fname, pg_compress_algorithm *compression) +{ + int fname_len = strlen(fname); + pg_compress_algorithm compress_algo; + Why do you need an extra variable here? I think compress_algo is better than compression. Besides that, it is a good opportunity to move this function to a common file (common/compression.c?) and use it in other places like CreateBackupStreamer() -- pg_basebackup. + /* 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) It reads better if you suppress 'Before that we must'. (I think you want to say 'After' instead of 'Before'.) + /* Hash table storing WAL entries read from the archive */ + ArchivedWAL_HTAB = ArchivedWAL_create(16, NULL); Any reason for 16? Comment says nothing about it. + if (read_archive_file(privateInfo, XLOG_BLCKSZ) == 0) + pg_fatal("could not find WAL in \"%s\" archive", + privateInfo->archive_name); Could we have a better message here? The read_archive_file() is already testing the error cases and you are testing the EOF case. I would use 'archive \"%s\"' or even 'archive file \"%s\". + /* Needed WAL yet to be decoded from archive, do the same */ + while (1) + { + entry = privateInfo->cur_wal; This comment is not clear. Could you rephrase it? + /* + * 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); Can it enforce a specific order? tar follows an arbitrary order in which the files is returned by the filesystem. You've been debating a solution to buffer the WAL contents using memory or spilled files. If it always create the tar in an alphabetical order, you can reduce the scope of this patch. (Didn't look what challenges are expected to use a sorted list to generate the tar file.) - dependencies: [frontend_code, lz4, zstd], + dependencies: [frontend_code, lz4, zstd, libpq], Put libpq after frontend_code. + + /* 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; It is a matter of style but consistency is good. Per current style snake case is preferred instead of CamelCase for these last members. - @lines = test_pg_waldump('--path' => $path); + my @lines; + @lines = test_pg_waldump($path); my @lines = test_pg_waldump($path); +/* Forward declaration */ +struct ArchivedWALFile; Why don't you move ArchivedWALFile to pg_waldump.h? pg_waldump.h is included into archive_waldump.c. 0006. As I said before I would avoid increasing the size of this patch if an ordered tar file is viable. Didn't look in deep this patch but I have a few suggestions: * I don't like tmpfile_exists as a member name. A suggestion is 'spilled'. +#ifndef WIN32 + if (chmod(fpath, pg_file_create_mode)) + pg_fatal("could not set permissions on file \"%s\": %m", + fpath); +#endif s/on/of/ My suggestion is to use the same sentence in initdb. could not change permissions of \"%s\": %m + pg_log_debug("temporarily exporting file \"%s\"", fpath); spilling to temporary file \"%s\" 0007. LGTM. 0008. I'm concerned about this patch. It is breaking backward compatibility if you are using a long option (--wal-directory). Your proposal is a generic word that represents both cases (file and directory). I agree. However, I wouldn't remove --wal-directory from the tool. Instead, I would keep it with the same short option ('w') but add a sentence saying this long option is deprecated and will be removed in the future or even remove any traces of this long option from the help and documentation but silently accept the old long option. I prefer the latter because it is not a required argument so a deprecation warning is not necessary IMO. 0009. LGTM. -- Euler Taveira EDB https://www.enterprisedb.com/ -
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2026-01-28T13:03:02Z
On Tue, Jan 27, 2026 at 10:02 PM Euler Taveira <euler@eulerto.com> wrote: > + * archive_waldump.c > + * A generic facility for reading WAL data from tar archives via archive > + * streamer. > > The other tools (pg_basebackup and pg_verifybackup) that also use astreamer API > named this similar file as astreamer_SOMETHING.c. It seems a good idea to > follow the same pattern, no? Maybe astreamer_tar_archive.c or > astreamer_archive.c. There shouldn't be anything specific to tar files in here, and astreamer_archive would be meaningless, since the "a" in "astreamer" stands for archive. What this file is is an archive streamer specific to pg_waldump, hence the name. > Can it enforce a specific order? tar follows an arbitrary order in which the > files is returned by the filesystem. You've been debating a solution to buffer > the WAL contents using memory or spilled files. If it always create the tar in > an alphabetical order, you can reduce the scope of this patch. (Didn't look > what challenges are expected to use a sorted list to generate the tar file.) It's posible to create a tar file in a specific order by specifying command-line arguments to tar in the order you want the tar file to be built. But I think the real thing here is that this limitation is lifted by the following patch. Whether it's worth splitting it apart into two patches this way is debatable. As I have pointed out in my previous reviews, the split hasn't been done very cleanly. -- Robert Haas EDB: http://www.enterprisedb.com
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-02-04T13:09:10Z
On Wed, Jan 28, 2026 at 2:41 AM Robert Haas <robertmhaas@gmail.com> wrote: > > On Tue, Jan 27, 2026 at 7:07 AM Amul Sul <sulamul@gmail.com> wrote: > > In the attached version, I am using the WAL segment name as the hash > > key, which is much more straightforward. I have rewritten > > read_archive_wal_page(), and it looks much cleaner than before. The > > logic to discard irrelevant WAL files is still within > > get_archive_wal_entry. I added an explanation for setting cur_wal to > > NULL, which is now handled in the separate function I mentioned > > previously. > > > > Kindly have a look at the attached version; let me know if you are > > still not happy with the current approach for filtering/discarding > > irrelevant WAL segments. It isn't much different from the previous > > version, but I have tried to keep it in a separate routine for better > > code readability, with comments to make it easier to understand. I > > also added a comment for ArchivedWALFile. > > I feel like the division of labor between get_archive_wal_entry() and > read_archive_wal_page() is odd. I noticed this in the last version, > too, and it still seems to be the case. get_archive_wal_entry() first > calls ArchivedWAL_lookup(). If that finds an entry, it just returns. > If it doesn't, it loops until an entry for the requested file shows up > and then returns it. Then control returns to read_archive_wal_page() > which loops some more until we have all the data we need for the > requested file. But it seems odd to me to have two separate loops > here. I think that the first loop is going to call read_archive_file() > until we find the beginning of the file that we care about and then > the second one is going to call read_archive_file() some more until we > have read enough of it to satisfy the request. It feels odd to me to > do it that way, as if we told somebody to first wait until 9 o'clock > and then wait another 30 minutes, instead of just telling them to wait > until 9:30. I realize it's not quite the same thing, because apart > from calling read_archive_file(), the two loops do different things, > but I still think it looks odd. > > + /* > + * Ignore if the timeline is different or the current segment is not > + * the desired one. > + */ > + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); > + if (privateInfo->timeline != curSegTimeline || > + privateInfo->startSegNo > curSegNo || > + privateInfo->endSegNo < curSegNo || > + segno > curSegNo) > + { > + free_archive_wal_entry(entry->fname, privateInfo); > + continue; > + } > > The comment doesn't match the code. If it did, the test would be > (privateInfo->timeline != curSegTimeline || segno != curSegno). But > instead the segno test is > rather than !=, and the checks against > startSegNo and endSegNo aren't explained at all. I think I understand > why the segno test uses > rather than !=, but it's the point of the > comment to explain things like that, rather than leaving the reader to > guess. And I don't know why we also need to test startSegNo and > endSegNo. > > I also wonder what the point is of doing XLogFromFileName() on the > fname provided by the caller and then again on entry->fname. Couldn't > you just compare the strings? > > Again, the division of labor is really odd here. It's the job of > astreamer_waldump_content() to skip things that aren't WAL files at > all, but it's the job of get_archive_wal_entry() to skip things that > are WAL files but not the one we want. I disagree with putting those > checks in completely separate parts of the code. > Keeping the timeline and segment start-end range checks inside the archive streamer creates a circular dependency that cannot be resolved without a 'dirty hack'. We must read the first available WAL file page to determine the wal_segment_size before it can calculate the target segment range. Moving the checks inside the streamer would make it impossible to process that initial file, as the necessary filtering parameters -- would still be unknown which would need to be skipped for the first read somehow. What if later we realized that the first WAL file which was allowed to be streamed by skipping that check is irrelevant and doesn't fall under the start-end segment range? > +static bool > +member_is_wal_file(astreamer_waldump *mystreamer, astreamer_member *member, > + char **fname) > +{ > + int pathlen; > + char *filename; > + > + /* 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 files from the top-level or pg_wal directory will be decoded */ > + if (pathlen > XLOG_FNAME_LEN && > + strncmp(member->pathname, XLOGDIR, strlen(XLOGDIR)) != 0) > + return false; > + > + /* WAL file could be with full path */ > + filename = member->pathname + (pathlen - XLOG_FNAME_LEN); > + if (!IsXLogFileName(filename)) > + return false; > + > + *fname = pnstrdup(filename, XLOG_FNAME_LEN); > + > + return true; > +} > > I don't think this code is fully correct. Note that pg_verifybackup's > member_verify_header() prepends "./" and then applies > canonicalize_path(). I think the same considerations apply here. > Consider: > Understood, will fix that. Regards, Amul -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-02-04T13:52:41Z
On Wed, Jan 28, 2026 at 8:32 AM Euler Taveira <euler@eulerto.com> wrote: > > On Tue, Jan 27, 2026, at 9:06 AM, Amul Sul wrote: > > Hi Euler, Thanks for looking into this, and apologies for the delayed response due to some unavoidable circumstances. The updated version of the patch could take a bit more time, as I am trying to fix and improve the code based on the concerns Robert raised in his previous post. > > I have included your patch (removing WalSegSz) to the attached patch > > set as 0001; all my subsequent patches are now bumped by one. > > > 0003. If this is a refactor, you should move the unlikely logic (if necessary) > to 0005. > Yes, that can be done. I have kept them separate for now to make the review easier. I can merge them once the patch is ready for the committer stage, unless the committer prefers to keep them as separate commit. > 0004. You mentioned in the commit message that this patch should be merged into > 0005. Reviewing it is a bit hard if you just looked at the patch. However, it > is clear if you use 'git show -w $HASH0004'. I would expect that this patch > contains > > +my $tar = $ENV{TAR}; > + > > ... > > + skip "tar command is not available", 3 > + if !defined $tar; > > because you add the SKIP. As a refactor I expect it to work independently. > Okay, will do it in the next updated version. > 0005. > > + pg_log_error("unnecessary command-line arguments specified with tar archive (first is \"%s\")", > + argv[optind]); > > Which command-line arguments? This message isn't clear. How about using "option > %s cannot be used together with tar archive". > In the error message, we simply print only the first one -- we do have similar error messages that use "command-line arguments". > - 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)) > > Maybe I'm confused about the fact that the function name is is_archive_file() > and there is a variable called is_archive. Couldn't you test > private.archive_name != NULL instead of using is_archive variable? > Yes, can do that. I hope that doesn't confuse anyone. > - if (!verify_directory(waldir)) > + else if (!verify_directory(walpath)) > { > pg_log_error("could not open directory \"%s\": %m", waldir); > goto bad_argument; > > This is not a problem of this patch but I just want to point out that it should > be pg_fatal() just like similar code path a few lines below. > > + /* set segno to endsegno for check of --end */ > + segno = endsegno; > + } > + > + > + if (!XLByteInSeg(private.endptr, segno, WalSegSz) && > + private.endptr != (segno + 1) * WalSegSz) > > 2 blank lines. Remove one. > That's pre-existing; I'm not sure if I should change this. > + * archive_waldump.c > + * A generic facility for reading WAL data from tar archives via archive > + * streamer. > > The other tools (pg_basebackup and pg_verifybackup) that also use astreamer API > named this similar file as astreamer_SOMETHING.c. It seems a good idea to > follow the same pattern, no? Maybe astreamer_tar_archive.c or > astreamer_archive.c. > Robert responded to this -- thanks, Robert. > +/* 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; > > s/wal file/WAL file/ > Okay. > What buf is for? It is the only member that doesn't have a description. I think > total_read gives the impression that you've read the file and that's the number > of bytes it got. That's not true; it represents the accumulative length. Maybe > read_len is a good candidate. > Okay. > +bool > +is_archive_file(const char *fname, pg_compress_algorithm *compression) > +{ > + int fname_len = strlen(fname); > + pg_compress_algorithm compress_algo; > + > > Why do you need an extra variable here? I think compress_algo is better than > compression. Well, I follow the practice of updating the output variable at the end to avoid any garbage assignment once all operations are successful. If there are strong objections to this, I am happy to change it. > Besides that, it is a good opportunity to move this function to a > common file (common/compression.c?) and use it in other places like > CreateBackupStreamer() -- pg_basebackup. > Yes, it would be a good idea to make it common, let me think about this. > + /* :. */ > + streamer = astreamer_tar_parser_new(streamer); > + > + /* Before that we must decompress, if archive is compressed. */ > + if (compression == PG_COMPRESSION_GZIP) > > It reads better if you suppress 'Before that we must'. (I think you want to say > 'After' instead of 'Before'.) > I meant to say 'Before'. This is the archive streamer stack, so the latter one is executed before it. > + /* Hash table storing WAL entries read from the archive */ > + ArchivedWAL_HTAB = ArchivedWAL_create(16, NULL); > > Any reason for 16? Comment says nothing about it. > It's an arbitrary choice; I will update the comment. > + if (read_archive_file(privateInfo, XLOG_BLCKSZ) == 0) > + pg_fatal("could not find WAL in \"%s\" archive", > + privateInfo->archive_name); > > Could we have a better message here? The read_archive_file() is already testing > the error cases and you are testing the EOF case. I would use 'archive \"%s\"' > or even 'archive file \"%s\". > Okay. > + /* Needed WAL yet to be decoded from archive, do the same */ > + while (1) > + { > + entry = privateInfo->cur_wal; > > This comment is not clear. Could you rephrase it? > Okay. > - dependencies: [frontend_code, lz4, zstd], > + dependencies: [frontend_code, lz4, zstd, libpq], > > Put libpq after frontend_code. > Okay. > + > + /* 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; > > It is a matter of style but consistency is good. Per current style snake case > is preferred instead of CamelCase for these last members. > Okay. > - @lines = test_pg_waldump('--path' => $path); > + my @lines; > + @lines = test_pg_waldump($path); > > my @lines = test_pg_waldump($path); > Okay. > +/* Forward declaration */ > +struct ArchivedWALFile; > > Why don't you move ArchivedWALFile to pg_waldump.h? pg_waldump.h is included > into archive_waldump.c. > The full structure is needed in archive_waldump.c only, so I added a forward declaration to declare the pointer inside XLogDumpPrivate. I believe we follow such practices elsewhere in the codebase as well. > 0006. As I said before I would avoid increasing the size of this patch if an > ordered tar file is viable. Didn't look in deep this patch but I have a few > suggestions: > > * I don't like tmpfile_exists as a member name. A suggestion is 'spilled'. > Okay. > +#ifndef WIN32 > + if (chmod(fpath, pg_file_create_mode)) > + pg_fatal("could not set permissions on file \"%s\": %m", > + fpath); > +#endif > > s/on/of/ > My suggestion is to use the same sentence in initdb. > I think 'on' seems to be much more relevant than 'of,' and we do have 'could not set permissions on' error messages in other places as wel > could not change permissions of \"%s\": %m > > + pg_log_debug("temporarily exporting file \"%s\"", fpath); > > spilling to temporary file \"%s\" > Okay. > 0008. I'm concerned about this patch. It is breaking backward compatibility if > you are using a long option (--wal-directory). Your proposal is a generic word > that represents both cases (file and directory). I agree. However, I wouldn't > remove --wal-directory from the tool. Instead, I would keep it with the same > short option ('w') but add a sentence saying this long option is deprecated and > will be removed in the future or even remove any traces of this long option > from the help and documentation but silently accept the old long option. I > prefer the latter because it is not a required argument so a deprecation > warning is not necessary IMO. > Yeah, that was discussed with Robert offline and we believe that it is better to make it more generalized; since we can now use the same option to accept both wal-directory and wal-archived. pg_waldump has much more generic options for the same, such as -- path=PATH. Regards, Amul -
Re: pg_waldump: support decoding of WAL inside tarfile
Robert Haas <robertmhaas@gmail.com> — 2026-02-07T13:04:32Z
On Wed, Feb 4, 2026 at 8:53 AM Amul Sul <sulamul@gmail.com> wrote: > > 0008. I'm concerned about this patch. It is breaking backward compatibility if > > you are using a long option (--wal-directory). Your proposal is a generic word > > that represents both cases (file and directory). I agree. However, I wouldn't > > remove --wal-directory from the tool. Instead, I would keep it with the same > > short option ('w') but add a sentence saying this long option is deprecated and > > will be removed in the future or even remove any traces of this long option > > from the help and documentation but silently accept the old long option. I > > prefer the latter because it is not a required argument so a deprecation > > warning is not necessary IMO. > > Yeah, that was discussed with Robert offline and we believe that it is > better to make it more generalized; since we can now use the same > option to accept both wal-directory and wal-archived. pg_waldump has > much more generic options for the same, such as -- path=PATH. Of course, the fact that we discussed it doesn't mean that the issue is completely settled. However, I don't think there would be general support from other people on the project for the idea of getting rid of the long option entirely, or even just the documentation for it. We have long options for almost all short options these days, and I agree with that as a general practice. Sometimes we have ONLY a long option, but we very rarely have ONLY a short option, which is good because sometimes we have tools with too many options for it to be viable to give everything a short option, and the long options really help to make things more self-documenting. What I would consider a more viable option is to not do the rename and leave this as --wal-directory even though the argument could really be a directory or a file. That would avoid the backward compatibility break that is troubling Euler. And, you could argue that a tar file is enough like a directory that it won't really cause much confusion. Personally, I favor renaming it. I think the number of people using pg_waldump is fairly small, and the number of those people who are using the long form of the option is very small. Hence, I don't believe think the rename will inconvenience many users, and I think it will improve clarity for future users. But, Euler (or someone else) might take the opposite viewpoint. -- Robert Haas EDB: http://www.enterprisedb.com -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-02-10T09:36:12Z
On Wed, Feb 4, 2026 at 6:39 PM Amul Sul <sulamul@gmail.com> wrote: > > On Wed, Jan 28, 2026 at 2:41 AM Robert Haas <robertmhaas@gmail.com> wrote: > > > > On Tue, Jan 27, 2026 at 7:07 AM Amul Sul <sulamul@gmail.com> wrote: > > > In the attached version, I am using the WAL segment name as the hash > > > key, which is much more straightforward. I have rewritten > > > read_archive_wal_page(), and it looks much cleaner than before. The > > > logic to discard irrelevant WAL files is still within > > > get_archive_wal_entry. I added an explanation for setting cur_wal to > > > NULL, which is now handled in the separate function I mentioned > > > previously. > > > > > > Kindly have a look at the attached version; let me know if you are > > > still not happy with the current approach for filtering/discarding > > > irrelevant WAL segments. It isn't much different from the previous > > > version, but I have tried to keep it in a separate routine for better > > > code readability, with comments to make it easier to understand. I > > > also added a comment for ArchivedWALFile. > > > > I feel like the division of labor between get_archive_wal_entry() and > > read_archive_wal_page() is odd. I noticed this in the last version, > > too, and it still seems to be the case. get_archive_wal_entry() first > > calls ArchivedWAL_lookup(). If that finds an entry, it just returns. > > If it doesn't, it loops until an entry for the requested file shows up > > and then returns it. Then control returns to read_archive_wal_page() > > which loops some more until we have all the data we need for the > > requested file. But it seems odd to me to have two separate loops > > here. I think that the first loop is going to call read_archive_file() > > until we find the beginning of the file that we care about and then > > the second one is going to call read_archive_file() some more until we > > have read enough of it to satisfy the request. It feels odd to me to > > do it that way, as if we told somebody to first wait until 9 o'clock > > and then wait another 30 minutes, instead of just telling them to wait > > until 9:30. I realize it's not quite the same thing, because apart > > from calling read_archive_file(), the two loops do different things, > > but I still think it looks odd. > > > > + /* > > + * Ignore if the timeline is different or the current segment is not > > + * the desired one. > > + */ > > + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); > > + if (privateInfo->timeline != curSegTimeline || > > + privateInfo->startSegNo > curSegNo || > > + privateInfo->endSegNo < curSegNo || > > + segno > curSegNo) > > + { > > + free_archive_wal_entry(entry->fname, privateInfo); > > + continue; > > + } > > > > The comment doesn't match the code. If it did, the test would be > > (privateInfo->timeline != curSegTimeline || segno != curSegno). But > > instead the segno test is > rather than !=, and the checks against > > startSegNo and endSegNo aren't explained at all. I think I understand > > why the segno test uses > rather than !=, but it's the point of the > > comment to explain things like that, rather than leaving the reader to > > guess. And I don't know why we also need to test startSegNo and > > endSegNo. > > > > I also wonder what the point is of doing XLogFromFileName() on the > > fname provided by the caller and then again on entry->fname. Couldn't > > you just compare the strings? > > > > Again, the division of labor is really odd here. It's the job of > > astreamer_waldump_content() to skip things that aren't WAL files at > > all, but it's the job of get_archive_wal_entry() to skip things that > > are WAL files but not the one we want. I disagree with putting those > > checks in completely separate parts of the code. > > > > Keeping the timeline and segment start-end range checks inside the > archive streamer creates a circular dependency that cannot be resolved > without a 'dirty hack'. We must read the first available WAL file page > to determine the wal_segment_size before it can calculate the target > segment range. Moving the checks inside the streamer would make it > impossible to process that initial file, as the necessary filtering > parameters -- would still be unknown which would need to be skipped > for the first read somehow. What if later we realized that the first > WAL file which was allowed to be streamed by skipping that check is > irrelevant and doesn't fall under the start-end segment range? > Please have a look at the attached version, specifically patch 0005. In astreamer_waldump_content(), I have moved the WAL file filtration check from get_archive_wal_entry(). This check will be skipped during the initial read in init_archive_reader(), which instead performs it explicitly once it determines the WAL segment size and the start/end segments. To access the WAL segment size inside astreamer_waldump_content(), I have moved the WAL segment size variable into the XLogDumpPrivate structure in the separate 0004 patch. Regards, Amul -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-02-18T06:58:27Z
On Tue, Feb 10, 2026 at 3:06 PM Amul Sul <sulamul@gmail.com> wrote: > > On Wed, Feb 4, 2026 at 6:39 PM Amul Sul <sulamul@gmail.com> wrote: > > > > On Wed, Jan 28, 2026 at 2:41 AM Robert Haas <robertmhaas@gmail.com> wrote: > > > > > > On Tue, Jan 27, 2026 at 7:07 AM Amul Sul <sulamul@gmail.com> wrote: > > > > In the attached version, I am using the WAL segment name as the hash > > > > key, which is much more straightforward. I have rewritten > > > > read_archive_wal_page(), and it looks much cleaner than before. The > > > > logic to discard irrelevant WAL files is still within > > > > get_archive_wal_entry. I added an explanation for setting cur_wal to > > > > NULL, which is now handled in the separate function I mentioned > > > > previously. > > > > > > > > Kindly have a look at the attached version; let me know if you are > > > > still not happy with the current approach for filtering/discarding > > > > irrelevant WAL segments. It isn't much different from the previous > > > > version, but I have tried to keep it in a separate routine for better > > > > code readability, with comments to make it easier to understand. I > > > > also added a comment for ArchivedWALFile. > > > > > > I feel like the division of labor between get_archive_wal_entry() and > > > read_archive_wal_page() is odd. I noticed this in the last version, > > > too, and it still seems to be the case. get_archive_wal_entry() first > > > calls ArchivedWAL_lookup(). If that finds an entry, it just returns. > > > If it doesn't, it loops until an entry for the requested file shows up > > > and then returns it. Then control returns to read_archive_wal_page() > > > which loops some more until we have all the data we need for the > > > requested file. But it seems odd to me to have two separate loops > > > here. I think that the first loop is going to call read_archive_file() > > > until we find the beginning of the file that we care about and then > > > the second one is going to call read_archive_file() some more until we > > > have read enough of it to satisfy the request. It feels odd to me to > > > do it that way, as if we told somebody to first wait until 9 o'clock > > > and then wait another 30 minutes, instead of just telling them to wait > > > until 9:30. I realize it's not quite the same thing, because apart > > > from calling read_archive_file(), the two loops do different things, > > > but I still think it looks odd. > > > > > > + /* > > > + * Ignore if the timeline is different or the current segment is not > > > + * the desired one. > > > + */ > > > + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); > > > + if (privateInfo->timeline != curSegTimeline || > > > + privateInfo->startSegNo > curSegNo || > > > + privateInfo->endSegNo < curSegNo || > > > + segno > curSegNo) > > > + { > > > + free_archive_wal_entry(entry->fname, privateInfo); > > > + continue; > > > + } > > > > > > The comment doesn't match the code. If it did, the test would be > > > (privateInfo->timeline != curSegTimeline || segno != curSegno). But > > > instead the segno test is > rather than !=, and the checks against > > > startSegNo and endSegNo aren't explained at all. I think I understand > > > why the segno test uses > rather than !=, but it's the point of the > > > comment to explain things like that, rather than leaving the reader to > > > guess. And I don't know why we also need to test startSegNo and > > > endSegNo. > > > > > > I also wonder what the point is of doing XLogFromFileName() on the > > > fname provided by the caller and then again on entry->fname. Couldn't > > > you just compare the strings? > > > > > > Again, the division of labor is really odd here. It's the job of > > > astreamer_waldump_content() to skip things that aren't WAL files at > > > all, but it's the job of get_archive_wal_entry() to skip things that > > > are WAL files but not the one we want. I disagree with putting those > > > checks in completely separate parts of the code. > > > > > > > Keeping the timeline and segment start-end range checks inside the > > archive streamer creates a circular dependency that cannot be resolved > > without a 'dirty hack'. We must read the first available WAL file page > > to determine the wal_segment_size before it can calculate the target > > segment range. Moving the checks inside the streamer would make it > > impossible to process that initial file, as the necessary filtering > > parameters -- would still be unknown which would need to be skipped > > for the first read somehow. What if later we realized that the first > > WAL file which was allowed to be streamed by skipping that check is > > irrelevant and doesn't fall under the start-end segment range? > > > > Please have a look at the attached version, specifically patch 0005. > In astreamer_waldump_content(), I have moved the WAL file filtration > check from get_archive_wal_entry(). This check will be skipped during > the initial read in init_archive_reader(), which instead performs it > explicitly once it determines the WAL segment size and the start/end > segments. > > To access the WAL segment size inside astreamer_waldump_content(), I > have moved the WAL segment size variable into the XLogDumpPrivate > structure in the separate 0004 patch. Attached is an updated version including the aforesaid changes. It includes a new refactoring patch (0001) that moves the logic for identifying tar archives and their compression types from pg_basebackup and pg_verifybackup into a separate-reusable function, per a suggestion from Euler [1]. Additionally, I have added a test for the contrecord decoding to the main patch (now 0006). 1] http://postgr.es/m/3c8e7b02-2152-495a-a0b6-e37cf9286a70@app.fastmail.com Regards, Amul -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-03-02T13:00:24Z
On Wed, Feb 18, 2026 at 12:28 PM Amul Sul <sulamul@gmail.com> wrote: > > On Tue, Feb 10, 2026 at 3:06 PM Amul Sul <sulamul@gmail.com> wrote: > > > > On Wed, Feb 4, 2026 at 6:39 PM Amul Sul <sulamul@gmail.com> wrote: > > > > > > On Wed, Jan 28, 2026 at 2:41 AM Robert Haas <robertmhaas@gmail.com> wrote: > > > > > > > > On Tue, Jan 27, 2026 at 7:07 AM Amul Sul <sulamul@gmail.com> wrote: > > > > > In the attached version, I am using the WAL segment name as the hash > > > > > key, which is much more straightforward. I have rewritten > > > > > read_archive_wal_page(), and it looks much cleaner than before. The > > > > > logic to discard irrelevant WAL files is still within > > > > > get_archive_wal_entry. I added an explanation for setting cur_wal to > > > > > NULL, which is now handled in the separate function I mentioned > > > > > previously. > > > > > > > > > > Kindly have a look at the attached version; let me know if you are > > > > > still not happy with the current approach for filtering/discarding > > > > > irrelevant WAL segments. It isn't much different from the previous > > > > > version, but I have tried to keep it in a separate routine for better > > > > > code readability, with comments to make it easier to understand. I > > > > > also added a comment for ArchivedWALFile. > > > > > > > > I feel like the division of labor between get_archive_wal_entry() and > > > > read_archive_wal_page() is odd. I noticed this in the last version, > > > > too, and it still seems to be the case. get_archive_wal_entry() first > > > > calls ArchivedWAL_lookup(). If that finds an entry, it just returns. > > > > If it doesn't, it loops until an entry for the requested file shows up > > > > and then returns it. Then control returns to read_archive_wal_page() > > > > which loops some more until we have all the data we need for the > > > > requested file. But it seems odd to me to have two separate loops > > > > here. I think that the first loop is going to call read_archive_file() > > > > until we find the beginning of the file that we care about and then > > > > the second one is going to call read_archive_file() some more until we > > > > have read enough of it to satisfy the request. It feels odd to me to > > > > do it that way, as if we told somebody to first wait until 9 o'clock > > > > and then wait another 30 minutes, instead of just telling them to wait > > > > until 9:30. I realize it's not quite the same thing, because apart > > > > from calling read_archive_file(), the two loops do different things, > > > > but I still think it looks odd. > > > > > > > > + /* > > > > + * Ignore if the timeline is different or the current segment is not > > > > + * the desired one. > > > > + */ > > > > + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); > > > > + if (privateInfo->timeline != curSegTimeline || > > > > + privateInfo->startSegNo > curSegNo || > > > > + privateInfo->endSegNo < curSegNo || > > > > + segno > curSegNo) > > > > + { > > > > + free_archive_wal_entry(entry->fname, privateInfo); > > > > + continue; > > > > + } > > > > > > > > The comment doesn't match the code. If it did, the test would be > > > > (privateInfo->timeline != curSegTimeline || segno != curSegno). But > > > > instead the segno test is > rather than !=, and the checks against > > > > startSegNo and endSegNo aren't explained at all. I think I understand > > > > why the segno test uses > rather than !=, but it's the point of the > > > > comment to explain things like that, rather than leaving the reader to > > > > guess. And I don't know why we also need to test startSegNo and > > > > endSegNo. > > > > > > > > I also wonder what the point is of doing XLogFromFileName() on the > > > > fname provided by the caller and then again on entry->fname. Couldn't > > > > you just compare the strings? > > > > > > > > Again, the division of labor is really odd here. It's the job of > > > > astreamer_waldump_content() to skip things that aren't WAL files at > > > > all, but it's the job of get_archive_wal_entry() to skip things that > > > > are WAL files but not the one we want. I disagree with putting those > > > > checks in completely separate parts of the code. > > > > > > > > > > Keeping the timeline and segment start-end range checks inside the > > > archive streamer creates a circular dependency that cannot be resolved > > > without a 'dirty hack'. We must read the first available WAL file page > > > to determine the wal_segment_size before it can calculate the target > > > segment range. Moving the checks inside the streamer would make it > > > impossible to process that initial file, as the necessary filtering > > > parameters -- would still be unknown which would need to be skipped > > > for the first read somehow. What if later we realized that the first > > > WAL file which was allowed to be streamed by skipping that check is > > > irrelevant and doesn't fall under the start-end segment range? > > > > > > > Please have a look at the attached version, specifically patch 0005. > > In astreamer_waldump_content(), I have moved the WAL file filtration > > check from get_archive_wal_entry(). This check will be skipped during > > the initial read in init_archive_reader(), which instead performs it > > explicitly once it determines the WAL segment size and the start/end > > segments. > > > > To access the WAL segment size inside astreamer_waldump_content(), I > > have moved the WAL segment size variable into the XLogDumpPrivate > > structure in the separate 0004 patch. > > Attached is an updated version including the aforesaid changes. It > includes a new refactoring patch (0001) that moves the logic for > identifying tar archives and their compression types from > pg_basebackup and pg_verifybackup into a separate-reusable function, > per a suggestion from Euler [1]. Additionally, I have added a test > for the contrecord decoding to the main patch (now 0006). > > 1] http://postgr.es/m/3c8e7b02-2152-495a-a0b6-e37cf9286a70@app.fastmail.com > Rebased against the latest master, fixed typos in code comments, and replaced palloc0 with palloc0_object. Regards, Amul -
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-03-04T00:37:48Z
On 2026-03-02 Mo 8:00 AM, Amul Sul wrote: > On Wed, Feb 18, 2026 at 12:28 PM Amul Sul <sulamul@gmail.com> wrote: >> On Tue, Feb 10, 2026 at 3:06 PM Amul Sul <sulamul@gmail.com> wrote: >>> On Wed, Feb 4, 2026 at 6:39 PM Amul Sul <sulamul@gmail.com> wrote: >>>> On Wed, Jan 28, 2026 at 2:41 AM Robert Haas <robertmhaas@gmail.com> wrote: >>>>> On Tue, Jan 27, 2026 at 7:07 AM Amul Sul <sulamul@gmail.com> wrote: >>>>>> In the attached version, I am using the WAL segment name as the hash >>>>>> key, which is much more straightforward. I have rewritten >>>>>> read_archive_wal_page(), and it looks much cleaner than before. The >>>>>> logic to discard irrelevant WAL files is still within >>>>>> get_archive_wal_entry. I added an explanation for setting cur_wal to >>>>>> NULL, which is now handled in the separate function I mentioned >>>>>> previously. >>>>>> >>>>>> Kindly have a look at the attached version; let me know if you are >>>>>> still not happy with the current approach for filtering/discarding >>>>>> irrelevant WAL segments. It isn't much different from the previous >>>>>> version, but I have tried to keep it in a separate routine for better >>>>>> code readability, with comments to make it easier to understand. I >>>>>> also added a comment for ArchivedWALFile. >>>>> I feel like the division of labor between get_archive_wal_entry() and >>>>> read_archive_wal_page() is odd. I noticed this in the last version, >>>>> too, and it still seems to be the case. get_archive_wal_entry() first >>>>> calls ArchivedWAL_lookup(). If that finds an entry, it just returns. >>>>> If it doesn't, it loops until an entry for the requested file shows up >>>>> and then returns it. Then control returns to read_archive_wal_page() >>>>> which loops some more until we have all the data we need for the >>>>> requested file. But it seems odd to me to have two separate loops >>>>> here. I think that the first loop is going to call read_archive_file() >>>>> until we find the beginning of the file that we care about and then >>>>> the second one is going to call read_archive_file() some more until we >>>>> have read enough of it to satisfy the request. It feels odd to me to >>>>> do it that way, as if we told somebody to first wait until 9 o'clock >>>>> and then wait another 30 minutes, instead of just telling them to wait >>>>> until 9:30. I realize it's not quite the same thing, because apart >>>>> from calling read_archive_file(), the two loops do different things, >>>>> but I still think it looks odd. >>>>> >>>>> + /* >>>>> + * Ignore if the timeline is different or the current segment is not >>>>> + * the desired one. >>>>> + */ >>>>> + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); >>>>> + if (privateInfo->timeline != curSegTimeline || >>>>> + privateInfo->startSegNo > curSegNo || >>>>> + privateInfo->endSegNo < curSegNo || >>>>> + segno > curSegNo) >>>>> + { >>>>> + free_archive_wal_entry(entry->fname, privateInfo); >>>>> + continue; >>>>> + } >>>>> >>>>> The comment doesn't match the code. If it did, the test would be >>>>> (privateInfo->timeline != curSegTimeline || segno != curSegno). But >>>>> instead the segno test is > rather than !=, and the checks against >>>>> startSegNo and endSegNo aren't explained at all. I think I understand >>>>> why the segno test uses > rather than !=, but it's the point of the >>>>> comment to explain things like that, rather than leaving the reader to >>>>> guess. And I don't know why we also need to test startSegNo and >>>>> endSegNo. >>>>> >>>>> I also wonder what the point is of doing XLogFromFileName() on the >>>>> fname provided by the caller and then again on entry->fname. Couldn't >>>>> you just compare the strings? >>>>> >>>>> Again, the division of labor is really odd here. It's the job of >>>>> astreamer_waldump_content() to skip things that aren't WAL files at >>>>> all, but it's the job of get_archive_wal_entry() to skip things that >>>>> are WAL files but not the one we want. I disagree with putting those >>>>> checks in completely separate parts of the code. >>>>> >>>> Keeping the timeline and segment start-end range checks inside the >>>> archive streamer creates a circular dependency that cannot be resolved >>>> without a 'dirty hack'. We must read the first available WAL file page >>>> to determine the wal_segment_size before it can calculate the target >>>> segment range. Moving the checks inside the streamer would make it >>>> impossible to process that initial file, as the necessary filtering >>>> parameters -- would still be unknown which would need to be skipped >>>> for the first read somehow. What if later we realized that the first >>>> WAL file which was allowed to be streamed by skipping that check is >>>> irrelevant and doesn't fall under the start-end segment range? >>>> >>> Please have a look at the attached version, specifically patch 0005. >>> In astreamer_waldump_content(), I have moved the WAL file filtration >>> check from get_archive_wal_entry(). This check will be skipped during >>> the initial read in init_archive_reader(), which instead performs it >>> explicitly once it determines the WAL segment size and the start/end >>> segments. >>> >>> To access the WAL segment size inside astreamer_waldump_content(), I >>> have moved the WAL segment size variable into the XLogDumpPrivate >>> structure in the separate 0004 patch. >> Attached is an updated version including the aforesaid changes. It >> includes a new refactoring patch (0001) that moves the logic for >> identifying tar archives and their compression types from >> pg_basebackup and pg_verifybackup into a separate-reusable function, >> per a suggestion from Euler [1]. Additionally, I have added a test >> for the contrecord decoding to the main patch (now 0006). >> >> 1] http://postgr.es/m/3c8e7b02-2152-495a-a0b6-e37cf9286a70@app.fastmail.com >> > Rebased against the latest master, fixed typos in code comments, and > replaced palloc0 with palloc0_object. > Hi Amul. I think this looks in pretty good shape. Attached are patches for a few things I think could be fixed. They are mostly self-explanatory. The TAP test fix is the only sane way I could come up with stopping the skip code you had from reporting a wildly inaccurate number of tests skipped. The sane way to do this from a Test::More perspective is a subtest, but unfortunately meson does not like subtest output, which is why we don't use it elsewhere, so the only way I could come up with was to split this out into a separate test. Of course, we might just say we don't care about the misreport, in which case we could just live with things as they are. cheers andrew -- Andrew Dunstan EDB: https://www.enterprisedb.com -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-03-04T12:52:00Z
On Wed, Mar 4, 2026 at 6:07 AM Andrew Dunstan <andrew@dunslane.net> wrote: > > > On 2026-03-02 Mo 8:00 AM, Amul Sul wrote: > > On Wed, Feb 18, 2026 at 12:28 PM Amul Sul <sulamul@gmail.com> wrote: > >> On Tue, Feb 10, 2026 at 3:06 PM Amul Sul <sulamul@gmail.com> wrote: > >>> On Wed, Feb 4, 2026 at 6:39 PM Amul Sul <sulamul@gmail.com> wrote: > >>>> On Wed, Jan 28, 2026 at 2:41 AM Robert Haas <robertmhaas@gmail.com> wrote: > >>>>> On Tue, Jan 27, 2026 at 7:07 AM Amul Sul <sulamul@gmail.com> wrote: > >>>>>> In the attached version, I am using the WAL segment name as the hash > >>>>>> key, which is much more straightforward. I have rewritten > >>>>>> read_archive_wal_page(), and it looks much cleaner than before. The > >>>>>> logic to discard irrelevant WAL files is still within > >>>>>> get_archive_wal_entry. I added an explanation for setting cur_wal to > >>>>>> NULL, which is now handled in the separate function I mentioned > >>>>>> previously. > >>>>>> > >>>>>> Kindly have a look at the attached version; let me know if you are > >>>>>> still not happy with the current approach for filtering/discarding > >>>>>> irrelevant WAL segments. It isn't much different from the previous > >>>>>> version, but I have tried to keep it in a separate routine for better > >>>>>> code readability, with comments to make it easier to understand. I > >>>>>> also added a comment for ArchivedWALFile. > >>>>> I feel like the division of labor between get_archive_wal_entry() and > >>>>> read_archive_wal_page() is odd. I noticed this in the last version, > >>>>> too, and it still seems to be the case. get_archive_wal_entry() first > >>>>> calls ArchivedWAL_lookup(). If that finds an entry, it just returns. > >>>>> If it doesn't, it loops until an entry for the requested file shows up > >>>>> and then returns it. Then control returns to read_archive_wal_page() > >>>>> which loops some more until we have all the data we need for the > >>>>> requested file. But it seems odd to me to have two separate loops > >>>>> here. I think that the first loop is going to call read_archive_file() > >>>>> until we find the beginning of the file that we care about and then > >>>>> the second one is going to call read_archive_file() some more until we > >>>>> have read enough of it to satisfy the request. It feels odd to me to > >>>>> do it that way, as if we told somebody to first wait until 9 o'clock > >>>>> and then wait another 30 minutes, instead of just telling them to wait > >>>>> until 9:30. I realize it's not quite the same thing, because apart > >>>>> from calling read_archive_file(), the two loops do different things, > >>>>> but I still think it looks odd. > >>>>> > >>>>> + /* > >>>>> + * Ignore if the timeline is different or the current segment is not > >>>>> + * the desired one. > >>>>> + */ > >>>>> + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); > >>>>> + if (privateInfo->timeline != curSegTimeline || > >>>>> + privateInfo->startSegNo > curSegNo || > >>>>> + privateInfo->endSegNo < curSegNo || > >>>>> + segno > curSegNo) > >>>>> + { > >>>>> + free_archive_wal_entry(entry->fname, privateInfo); > >>>>> + continue; > >>>>> + } > >>>>> > >>>>> The comment doesn't match the code. If it did, the test would be > >>>>> (privateInfo->timeline != curSegTimeline || segno != curSegno). But > >>>>> instead the segno test is > rather than !=, and the checks against > >>>>> startSegNo and endSegNo aren't explained at all. I think I understand > >>>>> why the segno test uses > rather than !=, but it's the point of the > >>>>> comment to explain things like that, rather than leaving the reader to > >>>>> guess. And I don't know why we also need to test startSegNo and > >>>>> endSegNo. > >>>>> > >>>>> I also wonder what the point is of doing XLogFromFileName() on the > >>>>> fname provided by the caller and then again on entry->fname. Couldn't > >>>>> you just compare the strings? > >>>>> > >>>>> Again, the division of labor is really odd here. It's the job of > >>>>> astreamer_waldump_content() to skip things that aren't WAL files at > >>>>> all, but it's the job of get_archive_wal_entry() to skip things that > >>>>> are WAL files but not the one we want. I disagree with putting those > >>>>> checks in completely separate parts of the code. > >>>>> > >>>> Keeping the timeline and segment start-end range checks inside the > >>>> archive streamer creates a circular dependency that cannot be resolved > >>>> without a 'dirty hack'. We must read the first available WAL file page > >>>> to determine the wal_segment_size before it can calculate the target > >>>> segment range. Moving the checks inside the streamer would make it > >>>> impossible to process that initial file, as the necessary filtering > >>>> parameters -- would still be unknown which would need to be skipped > >>>> for the first read somehow. What if later we realized that the first > >>>> WAL file which was allowed to be streamed by skipping that check is > >>>> irrelevant and doesn't fall under the start-end segment range? > >>>> > >>> Please have a look at the attached version, specifically patch 0005. > >>> In astreamer_waldump_content(), I have moved the WAL file filtration > >>> check from get_archive_wal_entry(). This check will be skipped during > >>> the initial read in init_archive_reader(), which instead performs it > >>> explicitly once it determines the WAL segment size and the start/end > >>> segments. > >>> > >>> To access the WAL segment size inside astreamer_waldump_content(), I > >>> have moved the WAL segment size variable into the XLogDumpPrivate > >>> structure in the separate 0004 patch. > >> Attached is an updated version including the aforesaid changes. It > >> includes a new refactoring patch (0001) that moves the logic for > >> identifying tar archives and their compression types from > >> pg_basebackup and pg_verifybackup into a separate-reusable function, > >> per a suggestion from Euler [1]. Additionally, I have added a test > >> for the contrecord decoding to the main patch (now 0006). > >> > >> 1] http://postgr.es/m/3c8e7b02-2152-495a-a0b6-e37cf9286a70@app.fastmail.com > >> > > Rebased against the latest master, fixed typos in code comments, and > > replaced palloc0 with palloc0_object. > > > > Hi Amul. > > > I think this looks in pretty good shape. > Thank you very much for looking at the patch. > Attached are patches for a few things I think could be fixed. They are > mostly self-explanatory. The TAP test fix is the only sane way I could > come up with stopping the skip code you had from reporting a wildly > inaccurate number of tests skipped. The sane way to do this from a > Test::More perspective is a subtest, but unfortunately meson does not > like subtest output, which is why we don't use it elsewhere, so the only > way I could come up with was to split this out into a separate test. Of > course, we might just say we don't care about the misreport, in which > case we could just live with things as they are. > I agree that the reported skip number was incorrect, and I have corrected it in the attached patch. I haven't applied your patch for the TAP test improvements yet because I wanted to double-check it first with you; the patch as it stood created duplicate tests already present in 001_basic.pl. To avoid this duplication, I have added a loop that performs tests for both plain and tar WAL directory inputs, similar to the approach used in pg_verifybackup for different compression type tests (e.g., 008_untar.pl, 010_client_untar.pl). I don't have any objection to doing so if you feel the duplication is acceptable, but I feel that using a loop for the tests in 001_basic.pl is a bit tidier. Let me know your thoughts. I have applied all your other patches but skipped the changes to pg_verifybackup.c from cf5955-fixes.patch.no-cfbot, as they seem unrelated or perhaps I have misunderstood them. Regards, Amul -
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-03-04T21:50:30Z
On 2026-03-04 We 7:52 AM, Amul Sul wrote: > On Wed, Mar 4, 2026 at 6:07 AM Andrew Dunstan<andrew@dunslane.net> wrote: >> >> On 2026-03-02 Mo 8:00 AM, Amul Sul wrote: >>> On Wed, Feb 18, 2026 at 12:28 PM Amul Sul<sulamul@gmail.com> wrote: >>>> On Tue, Feb 10, 2026 at 3:06 PM Amul Sul<sulamul@gmail.com> wrote: >>>>> On Wed, Feb 4, 2026 at 6:39 PM Amul Sul<sulamul@gmail.com> wrote: >>>>>> On Wed, Jan 28, 2026 at 2:41 AM Robert Haas<robertmhaas@gmail.com> wrote: >>>>>>> On Tue, Jan 27, 2026 at 7:07 AM Amul Sul<sulamul@gmail.com> wrote: >>>>>>>> In the attached version, I am using the WAL segment name as the hash >>>>>>>> key, which is much more straightforward. I have rewritten >>>>>>>> read_archive_wal_page(), and it looks much cleaner than before. The >>>>>>>> logic to discard irrelevant WAL files is still within >>>>>>>> get_archive_wal_entry. I added an explanation for setting cur_wal to >>>>>>>> NULL, which is now handled in the separate function I mentioned >>>>>>>> previously. >>>>>>>> >>>>>>>> Kindly have a look at the attached version; let me know if you are >>>>>>>> still not happy with the current approach for filtering/discarding >>>>>>>> irrelevant WAL segments. It isn't much different from the previous >>>>>>>> version, but I have tried to keep it in a separate routine for better >>>>>>>> code readability, with comments to make it easier to understand. I >>>>>>>> also added a comment for ArchivedWALFile. >>>>>>> I feel like the division of labor between get_archive_wal_entry() and >>>>>>> read_archive_wal_page() is odd. I noticed this in the last version, >>>>>>> too, and it still seems to be the case. get_archive_wal_entry() first >>>>>>> calls ArchivedWAL_lookup(). If that finds an entry, it just returns. >>>>>>> If it doesn't, it loops until an entry for the requested file shows up >>>>>>> and then returns it. Then control returns to read_archive_wal_page() >>>>>>> which loops some more until we have all the data we need for the >>>>>>> requested file. But it seems odd to me to have two separate loops >>>>>>> here. I think that the first loop is going to call read_archive_file() >>>>>>> until we find the beginning of the file that we care about and then >>>>>>> the second one is going to call read_archive_file() some more until we >>>>>>> have read enough of it to satisfy the request. It feels odd to me to >>>>>>> do it that way, as if we told somebody to first wait until 9 o'clock >>>>>>> and then wait another 30 minutes, instead of just telling them to wait >>>>>>> until 9:30. I realize it's not quite the same thing, because apart >>>>>>> from calling read_archive_file(), the two loops do different things, >>>>>>> but I still think it looks odd. >>>>>>> >>>>>>> + /* >>>>>>> + * Ignore if the timeline is different or the current segment is not >>>>>>> + * the desired one. >>>>>>> + */ >>>>>>> + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); >>>>>>> + if (privateInfo->timeline != curSegTimeline || >>>>>>> + privateInfo->startSegNo > curSegNo || >>>>>>> + privateInfo->endSegNo < curSegNo || >>>>>>> + segno > curSegNo) >>>>>>> + { >>>>>>> + free_archive_wal_entry(entry->fname, privateInfo); >>>>>>> + continue; >>>>>>> + } >>>>>>> >>>>>>> The comment doesn't match the code. If it did, the test would be >>>>>>> (privateInfo->timeline != curSegTimeline || segno != curSegno). But >>>>>>> instead the segno test is > rather than !=, and the checks against >>>>>>> startSegNo and endSegNo aren't explained at all. I think I understand >>>>>>> why the segno test uses > rather than !=, but it's the point of the >>>>>>> comment to explain things like that, rather than leaving the reader to >>>>>>> guess. And I don't know why we also need to test startSegNo and >>>>>>> endSegNo. >>>>>>> >>>>>>> I also wonder what the point is of doing XLogFromFileName() on the >>>>>>> fname provided by the caller and then again on entry->fname. Couldn't >>>>>>> you just compare the strings? >>>>>>> >>>>>>> Again, the division of labor is really odd here. It's the job of >>>>>>> astreamer_waldump_content() to skip things that aren't WAL files at >>>>>>> all, but it's the job of get_archive_wal_entry() to skip things that >>>>>>> are WAL files but not the one we want. I disagree with putting those >>>>>>> checks in completely separate parts of the code. >>>>>>> >>>>>> Keeping the timeline and segment start-end range checks inside the >>>>>> archive streamer creates a circular dependency that cannot be resolved >>>>>> without a 'dirty hack'. We must read the first available WAL file page >>>>>> to determine the wal_segment_size before it can calculate the target >>>>>> segment range. Moving the checks inside the streamer would make it >>>>>> impossible to process that initial file, as the necessary filtering >>>>>> parameters -- would still be unknown which would need to be skipped >>>>>> for the first read somehow. What if later we realized that the first >>>>>> WAL file which was allowed to be streamed by skipping that check is >>>>>> irrelevant and doesn't fall under the start-end segment range? >>>>>> >>>>> Please have a look at the attached version, specifically patch 0005. >>>>> In astreamer_waldump_content(), I have moved the WAL file filtration >>>>> check from get_archive_wal_entry(). This check will be skipped during >>>>> the initial read in init_archive_reader(), which instead performs it >>>>> explicitly once it determines the WAL segment size and the start/end >>>>> segments. >>>>> >>>>> To access the WAL segment size inside astreamer_waldump_content(), I >>>>> have moved the WAL segment size variable into the XLogDumpPrivate >>>>> structure in the separate 0004 patch. >>>> Attached is an updated version including the aforesaid changes. It >>>> includes a new refactoring patch (0001) that moves the logic for >>>> identifying tar archives and their compression types from >>>> pg_basebackup and pg_verifybackup into a separate-reusable function, >>>> per a suggestion from Euler [1]. Additionally, I have added a test >>>> for the contrecord decoding to the main patch (now 0006). >>>> >>>> 1]http://postgr.es/m/3c8e7b02-2152-495a-a0b6-e37cf9286a70@app.fastmail.com >>>> >>> Rebased against the latest master, fixed typos in code comments, and >>> replaced palloc0 with palloc0_object. >>> >> Hi Amul. >> >> >> I think this looks in pretty good shape. >> > Thank you very much for looking at the patch. > >> Attached are patches for a few things I think could be fixed. They are >> mostly self-explanatory. The TAP test fix is the only sane way I could >> come up with stopping the skip code you had from reporting a wildly >> inaccurate number of tests skipped. The sane way to do this from a >> Test::More perspective is a subtest, but unfortunately meson does not >> like subtest output, which is why we don't use it elsewhere, so the only >> way I could come up with was to split this out into a separate test. Of >> course, we might just say we don't care about the misreport, in which >> case we could just live with things as they are. >> > I agree that the reported skip number was incorrect, and I have > corrected it in the attached patch. I haven't applied your patch for > the TAP test improvements yet because I wanted to double-check it > first with you; the patch as it stood created duplicate tests already > present in 001_basic.pl. To avoid this duplication, I have added a > loop that performs tests for both plain and tar WAL directory inputs, > similar to the approach used in pg_verifybackup for different > compression type tests (e.g., 008_untar.pl, 010_client_untar.pl). I > don't have any objection to doing so if you feel the duplication is > acceptable, but I feel that using a loop for the tests in 001_basic.pl > is a bit tidier. Let me know your thoughts. I will take a look. > > I have applied all your other patches but skipped the changes to > pg_verifybackup.c from cf5955-fixes.patch.no-cfbot, as they seem > unrelated or perhaps I have misunderstood them. <brown-paper-bag> That's what I get for using a poorly written tool. cheers andrew -- Andrew Dunstan EDB:https://www.enterprisedb.com -
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-03-06T22:21:09Z
On 2026-03-04 We 4:50 PM, Andrew Dunstan wrote: > > > On 2026-03-04 We 7:52 AM, Amul Sul wrote: >> On Wed, Mar 4, 2026 at 6:07 AM Andrew Dunstan<andrew@dunslane.net> wrote: >>> On 2026-03-02 Mo 8:00 AM, Amul Sul wrote: >>>> On Wed, Feb 18, 2026 at 12:28 PM Amul Sul<sulamul@gmail.com> wrote: >>>>> On Tue, Feb 10, 2026 at 3:06 PM Amul Sul<sulamul@gmail.com> wrote: >>>>>> On Wed, Feb 4, 2026 at 6:39 PM Amul Sul<sulamul@gmail.com> wrote: >>>>>>> On Wed, Jan 28, 2026 at 2:41 AM Robert Haas<robertmhaas@gmail.com> wrote: >>>>>>>> On Tue, Jan 27, 2026 at 7:07 AM Amul Sul<sulamul@gmail.com> wrote: >>>>>>>>> In the attached version, I am using the WAL segment name as the hash >>>>>>>>> key, which is much more straightforward. I have rewritten >>>>>>>>> read_archive_wal_page(), and it looks much cleaner than before. The >>>>>>>>> logic to discard irrelevant WAL files is still within >>>>>>>>> get_archive_wal_entry. I added an explanation for setting cur_wal to >>>>>>>>> NULL, which is now handled in the separate function I mentioned >>>>>>>>> previously. >>>>>>>>> >>>>>>>>> Kindly have a look at the attached version; let me know if you are >>>>>>>>> still not happy with the current approach for filtering/discarding >>>>>>>>> irrelevant WAL segments. It isn't much different from the previous >>>>>>>>> version, but I have tried to keep it in a separate routine for better >>>>>>>>> code readability, with comments to make it easier to understand. I >>>>>>>>> also added a comment for ArchivedWALFile. >>>>>>>> I feel like the division of labor between get_archive_wal_entry() and >>>>>>>> read_archive_wal_page() is odd. I noticed this in the last version, >>>>>>>> too, and it still seems to be the case. get_archive_wal_entry() first >>>>>>>> calls ArchivedWAL_lookup(). If that finds an entry, it just returns. >>>>>>>> If it doesn't, it loops until an entry for the requested file shows up >>>>>>>> and then returns it. Then control returns to read_archive_wal_page() >>>>>>>> which loops some more until we have all the data we need for the >>>>>>>> requested file. But it seems odd to me to have two separate loops >>>>>>>> here. I think that the first loop is going to call read_archive_file() >>>>>>>> until we find the beginning of the file that we care about and then >>>>>>>> the second one is going to call read_archive_file() some more until we >>>>>>>> have read enough of it to satisfy the request. It feels odd to me to >>>>>>>> do it that way, as if we told somebody to first wait until 9 o'clock >>>>>>>> and then wait another 30 minutes, instead of just telling them to wait >>>>>>>> until 9:30. I realize it's not quite the same thing, because apart >>>>>>>> from calling read_archive_file(), the two loops do different things, >>>>>>>> but I still think it looks odd. >>>>>>>> >>>>>>>> + /* >>>>>>>> + * Ignore if the timeline is different or the current segment is not >>>>>>>> + * the desired one. >>>>>>>> + */ >>>>>>>> + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); >>>>>>>> + if (privateInfo->timeline != curSegTimeline || >>>>>>>> + privateInfo->startSegNo > curSegNo || >>>>>>>> + privateInfo->endSegNo < curSegNo || >>>>>>>> + segno > curSegNo) >>>>>>>> + { >>>>>>>> + free_archive_wal_entry(entry->fname, privateInfo); >>>>>>>> + continue; >>>>>>>> + } >>>>>>>> >>>>>>>> The comment doesn't match the code. If it did, the test would be >>>>>>>> (privateInfo->timeline != curSegTimeline || segno != curSegno). But >>>>>>>> instead the segno test is > rather than !=, and the checks against >>>>>>>> startSegNo and endSegNo aren't explained at all. I think I understand >>>>>>>> why the segno test uses > rather than !=, but it's the point of the >>>>>>>> comment to explain things like that, rather than leaving the reader to >>>>>>>> guess. And I don't know why we also need to test startSegNo and >>>>>>>> endSegNo. >>>>>>>> >>>>>>>> I also wonder what the point is of doing XLogFromFileName() on the >>>>>>>> fname provided by the caller and then again on entry->fname. Couldn't >>>>>>>> you just compare the strings? >>>>>>>> >>>>>>>> Again, the division of labor is really odd here. It's the job of >>>>>>>> astreamer_waldump_content() to skip things that aren't WAL files at >>>>>>>> all, but it's the job of get_archive_wal_entry() to skip things that >>>>>>>> are WAL files but not the one we want. I disagree with putting those >>>>>>>> checks in completely separate parts of the code. >>>>>>>> >>>>>>> Keeping the timeline and segment start-end range checks inside the >>>>>>> archive streamer creates a circular dependency that cannot be resolved >>>>>>> without a 'dirty hack'. We must read the first available WAL file page >>>>>>> to determine the wal_segment_size before it can calculate the target >>>>>>> segment range. Moving the checks inside the streamer would make it >>>>>>> impossible to process that initial file, as the necessary filtering >>>>>>> parameters -- would still be unknown which would need to be skipped >>>>>>> for the first read somehow. What if later we realized that the first >>>>>>> WAL file which was allowed to be streamed by skipping that check is >>>>>>> irrelevant and doesn't fall under the start-end segment range? >>>>>>> >>>>>> Please have a look at the attached version, specifically patch 0005. >>>>>> In astreamer_waldump_content(), I have moved the WAL file filtration >>>>>> check from get_archive_wal_entry(). This check will be skipped during >>>>>> the initial read in init_archive_reader(), which instead performs it >>>>>> explicitly once it determines the WAL segment size and the start/end >>>>>> segments. >>>>>> >>>>>> To access the WAL segment size inside astreamer_waldump_content(), I >>>>>> have moved the WAL segment size variable into the XLogDumpPrivate >>>>>> structure in the separate 0004 patch. >>>>> Attached is an updated version including the aforesaid changes. It >>>>> includes a new refactoring patch (0001) that moves the logic for >>>>> identifying tar archives and their compression types from >>>>> pg_basebackup and pg_verifybackup into a separate-reusable function, >>>>> per a suggestion from Euler [1]. Additionally, I have added a test >>>>> for the contrecord decoding to the main patch (now 0006). >>>>> >>>>> 1]http://postgr.es/m/3c8e7b02-2152-495a-a0b6-e37cf9286a70@app.fastmail.com >>>>> >>>> Rebased against the latest master, fixed typos in code comments, and >>>> replaced palloc0 with palloc0_object. >>>> >>> Hi Amul. >>> >>> >>> I think this looks in pretty good shape. >>> >> Thank you very much for looking at the patch. >> >>> Attached are patches for a few things I think could be fixed. They are >>> mostly self-explanatory. The TAP test fix is the only sane way I could >>> come up with stopping the skip code you had from reporting a wildly >>> inaccurate number of tests skipped. The sane way to do this from a >>> Test::More perspective is a subtest, but unfortunately meson does not >>> like subtest output, which is why we don't use it elsewhere, so the only >>> way I could come up with was to split this out into a separate test. Of >>> course, we might just say we don't care about the misreport, in which >>> case we could just live with things as they are. >>> >> I agree that the reported skip number was incorrect, and I have >> corrected it in the attached patch. I haven't applied your patch for >> the TAP test improvements yet because I wanted to double-check it >> first with you; the patch as it stood created duplicate tests already >> present in 001_basic.pl. To avoid this duplication, I have added a >> loop that performs tests for both plain and tar WAL directory inputs, >> similar to the approach used in pg_verifybackup for different >> compression type tests (e.g., 008_untar.pl, 010_client_untar.pl). I >> don't have any objection to doing so if you feel the duplication is >> acceptable, but I feel that using a loop for the tests in 001_basic.pl >> is a bit tidier. Let me know your thoughts. > > > I will take a look. > I'm ok, with doing it this way. It's just a bit fragile - if we add a test the number will be wrong. But maybe it's not worth worrying about. Everything else looks fairly good. The attached fixes a few relatively minor issues in v15. The main one is that it stops allocating/freeing a buffer every time we call read_archive_file() and instead adds a reusable buffer. It also adds back wal-directory as an undocumented alias of wal-path, to avoid breaking legacy scripts unnecessarily, and adds constness to the fname argument of pg_tar_compress_algorithm, as well as fixing some indentation and grammar issues. All in all I think we're in good shape. cheers andrew -- Andrew Dunstan EDB: https://www.enterprisedb.com -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-03-09T12:26:50Z
On Sat, Mar 7, 2026 at 3:51 AM Andrew Dunstan <andrew@dunslane.net> wrote: > > > On 2026-03-04 We 4:50 PM, Andrew Dunstan wrote: > > > > > > On 2026-03-04 We 7:52 AM, Amul Sul wrote: > >> On Wed, Mar 4, 2026 at 6:07 AM Andrew Dunstan<andrew@dunslane.net> wrote: > >>> On 2026-03-02 Mo 8:00 AM, Amul Sul wrote: > >>>> On Wed, Feb 18, 2026 at 12:28 PM Amul Sul<sulamul@gmail.com> wrote: > >>>>> On Tue, Feb 10, 2026 at 3:06 PM Amul Sul<sulamul@gmail.com> wrote: > >>>>>> On Wed, Feb 4, 2026 at 6:39 PM Amul Sul<sulamul@gmail.com> wrote: > >>>>>>> On Wed, Jan 28, 2026 at 2:41 AM Robert Haas<robertmhaas@gmail.com> wrote: > >>>>>>>> On Tue, Jan 27, 2026 at 7:07 AM Amul Sul<sulamul@gmail.com> wrote: > >>>>>>>>> In the attached version, I am using the WAL segment name as the hash > >>>>>>>>> key, which is much more straightforward. I have rewritten > >>>>>>>>> read_archive_wal_page(), and it looks much cleaner than before. The > >>>>>>>>> logic to discard irrelevant WAL files is still within > >>>>>>>>> get_archive_wal_entry. I added an explanation for setting cur_wal to > >>>>>>>>> NULL, which is now handled in the separate function I mentioned > >>>>>>>>> previously. > >>>>>>>>> > >>>>>>>>> Kindly have a look at the attached version; let me know if you are > >>>>>>>>> still not happy with the current approach for filtering/discarding > >>>>>>>>> irrelevant WAL segments. It isn't much different from the previous > >>>>>>>>> version, but I have tried to keep it in a separate routine for better > >>>>>>>>> code readability, with comments to make it easier to understand. I > >>>>>>>>> also added a comment for ArchivedWALFile. > >>>>>>>> I feel like the division of labor between get_archive_wal_entry() and > >>>>>>>> read_archive_wal_page() is odd. I noticed this in the last version, > >>>>>>>> too, and it still seems to be the case. get_archive_wal_entry() first > >>>>>>>> calls ArchivedWAL_lookup(). If that finds an entry, it just returns. > >>>>>>>> If it doesn't, it loops until an entry for the requested file shows up > >>>>>>>> and then returns it. Then control returns to read_archive_wal_page() > >>>>>>>> which loops some more until we have all the data we need for the > >>>>>>>> requested file. But it seems odd to me to have two separate loops > >>>>>>>> here. I think that the first loop is going to call read_archive_file() > >>>>>>>> until we find the beginning of the file that we care about and then > >>>>>>>> the second one is going to call read_archive_file() some more until we > >>>>>>>> have read enough of it to satisfy the request. It feels odd to me to > >>>>>>>> do it that way, as if we told somebody to first wait until 9 o'clock > >>>>>>>> and then wait another 30 minutes, instead of just telling them to wait > >>>>>>>> until 9:30. I realize it's not quite the same thing, because apart > >>>>>>>> from calling read_archive_file(), the two loops do different things, > >>>>>>>> but I still think it looks odd. > >>>>>>>> > >>>>>>>> + /* > >>>>>>>> + * Ignore if the timeline is different or the current segment is not > >>>>>>>> + * the desired one. > >>>>>>>> + */ > >>>>>>>> + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); > >>>>>>>> + if (privateInfo->timeline != curSegTimeline || > >>>>>>>> + privateInfo->startSegNo > curSegNo || > >>>>>>>> + privateInfo->endSegNo < curSegNo || > >>>>>>>> + segno > curSegNo) > >>>>>>>> + { > >>>>>>>> + free_archive_wal_entry(entry->fname, privateInfo); > >>>>>>>> + continue; > >>>>>>>> + } > >>>>>>>> > >>>>>>>> The comment doesn't match the code. If it did, the test would be > >>>>>>>> (privateInfo->timeline != curSegTimeline || segno != curSegno). But > >>>>>>>> instead the segno test is > rather than !=, and the checks against > >>>>>>>> startSegNo and endSegNo aren't explained at all. I think I understand > >>>>>>>> why the segno test uses > rather than !=, but it's the point of the > >>>>>>>> comment to explain things like that, rather than leaving the reader to > >>>>>>>> guess. And I don't know why we also need to test startSegNo and > >>>>>>>> endSegNo. > >>>>>>>> > >>>>>>>> I also wonder what the point is of doing XLogFromFileName() on the > >>>>>>>> fname provided by the caller and then again on entry->fname. Couldn't > >>>>>>>> you just compare the strings? > >>>>>>>> > >>>>>>>> Again, the division of labor is really odd here. It's the job of > >>>>>>>> astreamer_waldump_content() to skip things that aren't WAL files at > >>>>>>>> all, but it's the job of get_archive_wal_entry() to skip things that > >>>>>>>> are WAL files but not the one we want. I disagree with putting those > >>>>>>>> checks in completely separate parts of the code. > >>>>>>>> > >>>>>>> Keeping the timeline and segment start-end range checks inside the > >>>>>>> archive streamer creates a circular dependency that cannot be resolved > >>>>>>> without a 'dirty hack'. We must read the first available WAL file page > >>>>>>> to determine the wal_segment_size before it can calculate the target > >>>>>>> segment range. Moving the checks inside the streamer would make it > >>>>>>> impossible to process that initial file, as the necessary filtering > >>>>>>> parameters -- would still be unknown which would need to be skipped > >>>>>>> for the first read somehow. What if later we realized that the first > >>>>>>> WAL file which was allowed to be streamed by skipping that check is > >>>>>>> irrelevant and doesn't fall under the start-end segment range? > >>>>>>> > >>>>>> Please have a look at the attached version, specifically patch 0005. > >>>>>> In astreamer_waldump_content(), I have moved the WAL file filtration > >>>>>> check from get_archive_wal_entry(). This check will be skipped during > >>>>>> the initial read in init_archive_reader(), which instead performs it > >>>>>> explicitly once it determines the WAL segment size and the start/end > >>>>>> segments. > >>>>>> > >>>>>> To access the WAL segment size inside astreamer_waldump_content(), I > >>>>>> have moved the WAL segment size variable into the XLogDumpPrivate > >>>>>> structure in the separate 0004 patch. > >>>>> Attached is an updated version including the aforesaid changes. It > >>>>> includes a new refactoring patch (0001) that moves the logic for > >>>>> identifying tar archives and their compression types from > >>>>> pg_basebackup and pg_verifybackup into a separate-reusable function, > >>>>> per a suggestion from Euler [1]. Additionally, I have added a test > >>>>> for the contrecord decoding to the main patch (now 0006). > >>>>> > >>>>> 1]http://postgr.es/m/3c8e7b02-2152-495a-a0b6-e37cf9286a70@app.fastmail.com > >>>>> > >>>> Rebased against the latest master, fixed typos in code comments, and > >>>> replaced palloc0 with palloc0_object. > >>>> > >>> Hi Amul. > >>> > >>> > >>> I think this looks in pretty good shape. > >>> > >> Thank you very much for looking at the patch. > >> > >>> Attached are patches for a few things I think could be fixed. They are > >>> mostly self-explanatory. The TAP test fix is the only sane way I could > >>> come up with stopping the skip code you had from reporting a wildly > >>> inaccurate number of tests skipped. The sane way to do this from a > >>> Test::More perspective is a subtest, but unfortunately meson does not > >>> like subtest output, which is why we don't use it elsewhere, so the only > >>> way I could come up with was to split this out into a separate test. Of > >>> course, we might just say we don't care about the misreport, in which > >>> case we could just live with things as they are. > >>> > >> I agree that the reported skip number was incorrect, and I have > >> corrected it in the attached patch. I haven't applied your patch for > >> the TAP test improvements yet because I wanted to double-check it > >> first with you; the patch as it stood created duplicate tests already > >> present in 001_basic.pl. To avoid this duplication, I have added a > >> loop that performs tests for both plain and tar WAL directory inputs, > >> similar to the approach used in pg_verifybackup for different > >> compression type tests (e.g., 008_untar.pl, 010_client_untar.pl). I > >> don't have any objection to doing so if you feel the duplication is > >> acceptable, but I feel that using a loop for the tests in 001_basic.pl > >> is a bit tidier. Let me know your thoughts. > > > > > > I will take a look. > > > > I'm ok, with doing it this way. It's just a bit fragile - if we add a > test the number will be wrong. But maybe it's not worth worrying about. > > Everything else looks fairly good. The attached fixes a few relatively > minor issues in v15. The main one is that it stops allocating/freeing a > buffer every time we call read_archive_file() and instead adds a > reusable buffer. It also adds back wal-directory as an undocumented > alias of wal-path, to avoid breaking legacy scripts unnecessarily, and > adds constness to the fname argument of pg_tar_compress_algorithm, as > well as fixing some indentation and grammar issues. > > All in all I think we're in good shape. Thanks for the review. I have incorporated your suggested changes, with one exception: I have skipped the buffer reallocation code in read_archive_file(). Since we only handle two specific read sizes -- XLOG_BLCKSZ and READ_CHUNK_SIZE (128 KB, we defined in archive_waldump.c) -- dynamic reallocation seems unnecessary. Instead, I moved the allocation to init_archive_reader(), which now initializes a buffer at READ_CHUNK_SIZE. I also added an assertion in read_archive_file() to ensure that no read request exceeds this allocated capacity. Kindly have a look at the attached version and let me know your thoughts. Regards, Amul -
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-03-11T17:08:04Z
On 2026-03-09 Mo 8:26 AM, Amul Sul wrote: > On Sat, Mar 7, 2026 at 3:51 AM Andrew Dunstan<andrew@dunslane.net> wrote: >> >> On 2026-03-04 We 4:50 PM, Andrew Dunstan wrote: >>> >>> On 2026-03-04 We 7:52 AM, Amul Sul wrote: >>>> On Wed, Mar 4, 2026 at 6:07 AM Andrew Dunstan<andrew@dunslane.net> wrote: >>>>> On 2026-03-02 Mo 8:00 AM, Amul Sul wrote: >>>>>> On Wed, Feb 18, 2026 at 12:28 PM Amul Sul<sulamul@gmail.com> wrote: >>>>>>> On Tue, Feb 10, 2026 at 3:06 PM Amul Sul<sulamul@gmail.com> wrote: >>>>>>>> On Wed, Feb 4, 2026 at 6:39 PM Amul Sul<sulamul@gmail.com> wrote: >>>>>>>>> On Wed, Jan 28, 2026 at 2:41 AM Robert Haas<robertmhaas@gmail.com> wrote: >>>>>>>>>> On Tue, Jan 27, 2026 at 7:07 AM Amul Sul<sulamul@gmail.com> wrote: >>>>>>>>>>> In the attached version, I am using the WAL segment name as the hash >>>>>>>>>>> key, which is much more straightforward. I have rewritten >>>>>>>>>>> read_archive_wal_page(), and it looks much cleaner than before. The >>>>>>>>>>> logic to discard irrelevant WAL files is still within >>>>>>>>>>> get_archive_wal_entry. I added an explanation for setting cur_wal to >>>>>>>>>>> NULL, which is now handled in the separate function I mentioned >>>>>>>>>>> previously. >>>>>>>>>>> >>>>>>>>>>> Kindly have a look at the attached version; let me know if you are >>>>>>>>>>> still not happy with the current approach for filtering/discarding >>>>>>>>>>> irrelevant WAL segments. It isn't much different from the previous >>>>>>>>>>> version, but I have tried to keep it in a separate routine for better >>>>>>>>>>> code readability, with comments to make it easier to understand. I >>>>>>>>>>> also added a comment for ArchivedWALFile. >>>>>>>>>> I feel like the division of labor between get_archive_wal_entry() and >>>>>>>>>> read_archive_wal_page() is odd. I noticed this in the last version, >>>>>>>>>> too, and it still seems to be the case. get_archive_wal_entry() first >>>>>>>>>> calls ArchivedWAL_lookup(). If that finds an entry, it just returns. >>>>>>>>>> If it doesn't, it loops until an entry for the requested file shows up >>>>>>>>>> and then returns it. Then control returns to read_archive_wal_page() >>>>>>>>>> which loops some more until we have all the data we need for the >>>>>>>>>> requested file. But it seems odd to me to have two separate loops >>>>>>>>>> here. I think that the first loop is going to call read_archive_file() >>>>>>>>>> until we find the beginning of the file that we care about and then >>>>>>>>>> the second one is going to call read_archive_file() some more until we >>>>>>>>>> have read enough of it to satisfy the request. It feels odd to me to >>>>>>>>>> do it that way, as if we told somebody to first wait until 9 o'clock >>>>>>>>>> and then wait another 30 minutes, instead of just telling them to wait >>>>>>>>>> until 9:30. I realize it's not quite the same thing, because apart >>>>>>>>>> from calling read_archive_file(), the two loops do different things, >>>>>>>>>> but I still think it looks odd. >>>>>>>>>> >>>>>>>>>> + /* >>>>>>>>>> + * Ignore if the timeline is different or the current segment is not >>>>>>>>>> + * the desired one. >>>>>>>>>> + */ >>>>>>>>>> + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); >>>>>>>>>> + if (privateInfo->timeline != curSegTimeline || >>>>>>>>>> + privateInfo->startSegNo > curSegNo || >>>>>>>>>> + privateInfo->endSegNo < curSegNo || >>>>>>>>>> + segno > curSegNo) >>>>>>>>>> + { >>>>>>>>>> + free_archive_wal_entry(entry->fname, privateInfo); >>>>>>>>>> + continue; >>>>>>>>>> + } >>>>>>>>>> >>>>>>>>>> The comment doesn't match the code. If it did, the test would be >>>>>>>>>> (privateInfo->timeline != curSegTimeline || segno != curSegno). But >>>>>>>>>> instead the segno test is > rather than !=, and the checks against >>>>>>>>>> startSegNo and endSegNo aren't explained at all. I think I understand >>>>>>>>>> why the segno test uses > rather than !=, but it's the point of the >>>>>>>>>> comment to explain things like that, rather than leaving the reader to >>>>>>>>>> guess. And I don't know why we also need to test startSegNo and >>>>>>>>>> endSegNo. >>>>>>>>>> >>>>>>>>>> I also wonder what the point is of doing XLogFromFileName() on the >>>>>>>>>> fname provided by the caller and then again on entry->fname. Couldn't >>>>>>>>>> you just compare the strings? >>>>>>>>>> >>>>>>>>>> Again, the division of labor is really odd here. It's the job of >>>>>>>>>> astreamer_waldump_content() to skip things that aren't WAL files at >>>>>>>>>> all, but it's the job of get_archive_wal_entry() to skip things that >>>>>>>>>> are WAL files but not the one we want. I disagree with putting those >>>>>>>>>> checks in completely separate parts of the code. >>>>>>>>>> >>>>>>>>> Keeping the timeline and segment start-end range checks inside the >>>>>>>>> archive streamer creates a circular dependency that cannot be resolved >>>>>>>>> without a 'dirty hack'. We must read the first available WAL file page >>>>>>>>> to determine the wal_segment_size before it can calculate the target >>>>>>>>> segment range. Moving the checks inside the streamer would make it >>>>>>>>> impossible to process that initial file, as the necessary filtering >>>>>>>>> parameters -- would still be unknown which would need to be skipped >>>>>>>>> for the first read somehow. What if later we realized that the first >>>>>>>>> WAL file which was allowed to be streamed by skipping that check is >>>>>>>>> irrelevant and doesn't fall under the start-end segment range? >>>>>>>>> >>>>>>>> Please have a look at the attached version, specifically patch 0005. >>>>>>>> In astreamer_waldump_content(), I have moved the WAL file filtration >>>>>>>> check from get_archive_wal_entry(). This check will be skipped during >>>>>>>> the initial read in init_archive_reader(), which instead performs it >>>>>>>> explicitly once it determines the WAL segment size and the start/end >>>>>>>> segments. >>>>>>>> >>>>>>>> To access the WAL segment size inside astreamer_waldump_content(), I >>>>>>>> have moved the WAL segment size variable into the XLogDumpPrivate >>>>>>>> structure in the separate 0004 patch. >>>>>>> Attached is an updated version including the aforesaid changes. It >>>>>>> includes a new refactoring patch (0001) that moves the logic for >>>>>>> identifying tar archives and their compression types from >>>>>>> pg_basebackup and pg_verifybackup into a separate-reusable function, >>>>>>> per a suggestion from Euler [1]. Additionally, I have added a test >>>>>>> for the contrecord decoding to the main patch (now 0006). >>>>>>> >>>>>>> 1]http://postgr.es/m/3c8e7b02-2152-495a-a0b6-e37cf9286a70@app.fastmail.com >>>>>>> >>>>>> Rebased against the latest master, fixed typos in code comments, and >>>>>> replaced palloc0 with palloc0_object. >>>>>> >>>>> Hi Amul. >>>>> >>>>> >>>>> I think this looks in pretty good shape. >>>>> >>>> Thank you very much for looking at the patch. >>>> >>>>> Attached are patches for a few things I think could be fixed. They are >>>>> mostly self-explanatory. The TAP test fix is the only sane way I could >>>>> come up with stopping the skip code you had from reporting a wildly >>>>> inaccurate number of tests skipped. The sane way to do this from a >>>>> Test::More perspective is a subtest, but unfortunately meson does not >>>>> like subtest output, which is why we don't use it elsewhere, so the only >>>>> way I could come up with was to split this out into a separate test. Of >>>>> course, we might just say we don't care about the misreport, in which >>>>> case we could just live with things as they are. >>>>> >>>> I agree that the reported skip number was incorrect, and I have >>>> corrected it in the attached patch. I haven't applied your patch for >>>> the TAP test improvements yet because I wanted to double-check it >>>> first with you; the patch as it stood created duplicate tests already >>>> present in 001_basic.pl. To avoid this duplication, I have added a >>>> loop that performs tests for both plain and tar WAL directory inputs, >>>> similar to the approach used in pg_verifybackup for different >>>> compression type tests (e.g., 008_untar.pl, 010_client_untar.pl). I >>>> don't have any objection to doing so if you feel the duplication is >>>> acceptable, but I feel that using a loop for the tests in 001_basic.pl >>>> is a bit tidier. Let me know your thoughts. >>> >>> I will take a look. >>> >> I'm ok, with doing it this way. It's just a bit fragile - if we add a >> test the number will be wrong. But maybe it's not worth worrying about. >> >> Everything else looks fairly good. The attached fixes a few relatively >> minor issues in v15. The main one is that it stops allocating/freeing a >> buffer every time we call read_archive_file() and instead adds a >> reusable buffer. It also adds back wal-directory as an undocumented >> alias of wal-path, to avoid breaking legacy scripts unnecessarily, and >> adds constness to the fname argument of pg_tar_compress_algorithm, as >> well as fixing some indentation and grammar issues. >> >> All in all I think we're in good shape. > Thanks for the review. I have incorporated your suggested changes, > with one exception: I have skipped the buffer reallocation code in > read_archive_file(). Since we only handle two specific read sizes -- > XLOG_BLCKSZ and READ_CHUNK_SIZE (128 KB, we defined in > archive_waldump.c) -- dynamic reallocation seems unnecessary. Instead, > I moved the allocation to init_archive_reader(), which now initializes > a buffer at READ_CHUNK_SIZE. I also added an assertion in > read_archive_file() to ensure that no read request exceeds this > allocated capacity. > > Kindly have a look at the attached version and let me know your thoughts. > Looks pretty good. I have squashed them into three patches I think are committable. Also attached is a diff showing what's changed - mainly this: . --follow + tar archive rejected (pg_waldump.c) — new validation prevents a confusing pg_fatal when combining --follow with a tar archive . error messages split (archive_waldump.c) — the single "could not read file" error is now two distinct messages: "WAL segment is too short" (truncated file) vs "unexpected end of archive" (archive EOF) - Fixes an issue raised in review . hash table cleanup (archive_waldump.c) — free_archive_reader now iterates and frees all remaining hash entries and destroys the table cheers andrew -- Andrew Dunstan EDB:https://www.enterprisedb.com -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-03-18T11:45:47Z
On Wed, Mar 11, 2026 at 10:38 PM Andrew Dunstan <andrew@dunslane.net> wrote: > > > On 2026-03-09 Mo 8:26 AM, Amul Sul wrote: > > On Sat, Mar 7, 2026 at 3:51 AM Andrew Dunstan <andrew@dunslane.net> wrote: > > On 2026-03-04 We 4:50 PM, Andrew Dunstan wrote: > > On 2026-03-04 We 7:52 AM, Amul Sul wrote: > > On Wed, Mar 4, 2026 at 6:07 AM Andrew Dunstan<andrew@dunslane.net> wrote: > > On 2026-03-02 Mo 8:00 AM, Amul Sul wrote: > > On Wed, Feb 18, 2026 at 12:28 PM Amul Sul<sulamul@gmail.com> wrote: > > On Tue, Feb 10, 2026 at 3:06 PM Amul Sul<sulamul@gmail.com> wrote: > > On Wed, Feb 4, 2026 at 6:39 PM Amul Sul<sulamul@gmail.com> wrote: > > On Wed, Jan 28, 2026 at 2:41 AM Robert Haas<robertmhaas@gmail.com> wrote: > > On Tue, Jan 27, 2026 at 7:07 AM Amul Sul<sulamul@gmail.com> wrote: > > In the attached version, I am using the WAL segment name as the hash > key, which is much more straightforward. I have rewritten > read_archive_wal_page(), and it looks much cleaner than before. The > logic to discard irrelevant WAL files is still within > get_archive_wal_entry. I added an explanation for setting cur_wal to > NULL, which is now handled in the separate function I mentioned > previously. > > Kindly have a look at the attached version; let me know if you are > still not happy with the current approach for filtering/discarding > irrelevant WAL segments. It isn't much different from the previous > version, but I have tried to keep it in a separate routine for better > code readability, with comments to make it easier to understand. I > also added a comment for ArchivedWALFile. > > I feel like the division of labor between get_archive_wal_entry() and > read_archive_wal_page() is odd. I noticed this in the last version, > too, and it still seems to be the case. get_archive_wal_entry() first > calls ArchivedWAL_lookup(). If that finds an entry, it just returns. > If it doesn't, it loops until an entry for the requested file shows up > and then returns it. Then control returns to read_archive_wal_page() > which loops some more until we have all the data we need for the > requested file. But it seems odd to me to have two separate loops > here. I think that the first loop is going to call read_archive_file() > until we find the beginning of the file that we care about and then > the second one is going to call read_archive_file() some more until we > have read enough of it to satisfy the request. It feels odd to me to > do it that way, as if we told somebody to first wait until 9 o'clock > and then wait another 30 minutes, instead of just telling them to wait > until 9:30. I realize it's not quite the same thing, because apart > from calling read_archive_file(), the two loops do different things, > but I still think it looks odd. > > + /* > + * Ignore if the timeline is different or the current segment is not > + * the desired one. > + */ > + XLogFromFileName(entry->fname, &curSegTimeline, &curSegNo, WalSegSz); > + if (privateInfo->timeline != curSegTimeline || > + privateInfo->startSegNo > curSegNo || > + privateInfo->endSegNo < curSegNo || > + segno > curSegNo) > + { > + free_archive_wal_entry(entry->fname, privateInfo); > + continue; > + } > > The comment doesn't match the code. If it did, the test would be > (privateInfo->timeline != curSegTimeline || segno != curSegno). But > instead the segno test is > rather than !=, and the checks against > startSegNo and endSegNo aren't explained at all. I think I understand > why the segno test uses > rather than !=, but it's the point of the > comment to explain things like that, rather than leaving the reader to > guess. And I don't know why we also need to test startSegNo and > endSegNo. > > I also wonder what the point is of doing XLogFromFileName() on the > fname provided by the caller and then again on entry->fname. Couldn't > you just compare the strings? > > Again, the division of labor is really odd here. It's the job of > astreamer_waldump_content() to skip things that aren't WAL files at > all, but it's the job of get_archive_wal_entry() to skip things that > are WAL files but not the one we want. I disagree with putting those > checks in completely separate parts of the code. > > Keeping the timeline and segment start-end range checks inside the > archive streamer creates a circular dependency that cannot be resolved > without a 'dirty hack'. We must read the first available WAL file page > to determine the wal_segment_size before it can calculate the target > segment range. Moving the checks inside the streamer would make it > impossible to process that initial file, as the necessary filtering > parameters -- would still be unknown which would need to be skipped > for the first read somehow. What if later we realized that the first > WAL file which was allowed to be streamed by skipping that check is > irrelevant and doesn't fall under the start-end segment range? > > Please have a look at the attached version, specifically patch 0005. > In astreamer_waldump_content(), I have moved the WAL file filtration > check from get_archive_wal_entry(). This check will be skipped during > the initial read in init_archive_reader(), which instead performs it > explicitly once it determines the WAL segment size and the start/end > segments. > > To access the WAL segment size inside astreamer_waldump_content(), I > have moved the WAL segment size variable into the XLogDumpPrivate > structure in the separate 0004 patch. > > Attached is an updated version including the aforesaid changes. It > includes a new refactoring patch (0001) that moves the logic for > identifying tar archives and their compression types from > pg_basebackup and pg_verifybackup into a separate-reusable function, > per a suggestion from Euler [1]. Additionally, I have added a test > for the contrecord decoding to the main patch (now 0006). > > 1]http://postgr.es/m/3c8e7b02-2152-495a-a0b6-e37cf9286a70@app.fastmail.com > > Rebased against the latest master, fixed typos in code comments, and > replaced palloc0 with palloc0_object. > > Hi Amul. > > > I think this looks in pretty good shape. > > Thank you very much for looking at the patch. > > Attached are patches for a few things I think could be fixed. They are > mostly self-explanatory. The TAP test fix is the only sane way I could > come up with stopping the skip code you had from reporting a wildly > inaccurate number of tests skipped. The sane way to do this from a > Test::More perspective is a subtest, but unfortunately meson does not > like subtest output, which is why we don't use it elsewhere, so the only > way I could come up with was to split this out into a separate test. Of > course, we might just say we don't care about the misreport, in which > case we could just live with things as they are. > > I agree that the reported skip number was incorrect, and I have > corrected it in the attached patch. I haven't applied your patch for > the TAP test improvements yet because I wanted to double-check it > first with you; the patch as it stood created duplicate tests already > present in 001_basic.pl. To avoid this duplication, I have added a > loop that performs tests for both plain and tar WAL directory inputs, > similar to the approach used in pg_verifybackup for different > compression type tests (e.g., 008_untar.pl, 010_client_untar.pl). I > don't have any objection to doing so if you feel the duplication is > acceptable, but I feel that using a loop for the tests in 001_basic.pl > is a bit tidier. Let me know your thoughts. > > I will take a look. > > I'm ok, with doing it this way. It's just a bit fragile - if we add a > test the number will be wrong. But maybe it's not worth worrying about. > > Everything else looks fairly good. The attached fixes a few relatively > minor issues in v15. The main one is that it stops allocating/freeing a > buffer every time we call read_archive_file() and instead adds a > reusable buffer. It also adds back wal-directory as an undocumented > alias of wal-path, to avoid breaking legacy scripts unnecessarily, and > adds constness to the fname argument of pg_tar_compress_algorithm, as > well as fixing some indentation and grammar issues. > > All in all I think we're in good shape. > > Thanks for the review. I have incorporated your suggested changes, > with one exception: I have skipped the buffer reallocation code in > read_archive_file(). Since we only handle two specific read sizes -- > XLOG_BLCKSZ and READ_CHUNK_SIZE (128 KB, we defined in > archive_waldump.c) -- dynamic reallocation seems unnecessary. Instead, > I moved the allocation to init_archive_reader(), which now initializes > a buffer at READ_CHUNK_SIZE. I also added an assertion in > read_archive_file() to ensure that no read request exceeds this > allocated capacity. > > Kindly have a look at the attached version and let me know your thoughts. > > > Looks pretty good. I have squashed them into three patches I think are committable. Also attached is a diff showing what's changed - mainly this: > > . --follow + tar archive rejected (pg_waldump.c) — new validation prevents a confusing pg_fatal when combining --follow with a tar archive > . error messages split (archive_waldump.c) — the single "could not read file" error is now two distinct messages: "WAL segment is too short" (truncated file) vs "unexpected end of archive" (archive EOF) - Fixes an issue raised in review > . hash table cleanup (archive_waldump.c) — free_archive_reader now iterates and frees all remaining hash entries and destroys the table > The final squashed version looks good to me, thank you. But, I would like to propose splitting the 0001 patch into two separate commits: a preparatory refactoring of the pg_waldump code and a standalone commit that moves the tar archive detection and compression logic to a common location, as the latter is an independent improvement to the existing codebase. Additionally, since the test file refactoring was only kept separate to facilitate the review and has already been reviewed, I suggest merging those changes into the main feature patch i.e. 0002. All other elements should remain in a single preparatory refactoring patch for pg_waldump. Attached is the version that includes the proposed split. No additional changes to 0002 and 0003 patches. Regards, Amul -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-03-18T15:16:35Z
On Wed, Mar 18, 2026 at 5:15 PM Amul Sul <sulamul@gmail.com> wrote: > > On Wed, Mar 11, 2026 at 10:38 PM Andrew Dunstan <andrew@dunslane.net> wrote: > > [...] > > Looks pretty good. I have squashed them into three patches I think are committable. Also attached is a diff showing what's changed - mainly this: > > > > . --follow + tar archive rejected (pg_waldump.c) — new validation prevents a confusing pg_fatal when combining --follow with a tar archive > > . error messages split (archive_waldump.c) — the single "could not read file" error is now two distinct messages: "WAL segment is too short" (truncated file) vs "unexpected end of archive" (archive EOF) - Fixes an issue raised in review > > . hash table cleanup (archive_waldump.c) — free_archive_reader now iterates and frees all remaining hash entries and destroys the table > > > > The final squashed version looks good to me, thank you. But, I would > like to propose splitting the 0001 patch into two separate commits: a > preparatory refactoring of the pg_waldump code and a standalone commit > that moves the tar archive detection and compression logic to a common > location, as the latter is an independent improvement to the existing > codebase. Additionally, since the test file refactoring was only kept > separate to facilitate the review and has already been reviewed, I > suggest merging those changes into the main feature patch i.e. 0002. > All other elements should remain in a single preparatory refactoring > patch for pg_waldump. > > Attached is the version that includes the proposed split. No > additional changes to 0002 and 0003 patches. > Added the two missing 'Reviewed-by' lines to the credit section of the commit message and did a minor optimization in get_archive_wal_entry. Regards, Amul
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-03-19T10:20:59Z
On Wed, Mar 18, 2026 at 8:46 PM Amul Sul <sulamul@gmail.com> wrote: > > On Wed, Mar 18, 2026 at 5:15 PM Amul Sul <sulamul@gmail.com> wrote: > > > > On Wed, Mar 11, 2026 at 10:38 PM Andrew Dunstan <andrew@dunslane.net> wrote: > > > [...] > > > Looks pretty good. I have squashed them into three patches I think are committable. Also attached is a diff showing what's changed - mainly this: > > > > > > . --follow + tar archive rejected (pg_waldump.c) — new validation prevents a confusing pg_fatal when combining --follow with a tar archive > > > . error messages split (archive_waldump.c) — the single "could not read file" error is now two distinct messages: "WAL segment is too short" (truncated file) vs "unexpected end of archive" (archive EOF) - Fixes an issue raised in review > > > . hash table cleanup (archive_waldump.c) — free_archive_reader now iterates and frees all remaining hash entries and destroys the table > > > > > > > The final squashed version looks good to me, thank you. But, I would > > like to propose splitting the 0001 patch into two separate commits: a > > preparatory refactoring of the pg_waldump code and a standalone commit > > that moves the tar archive detection and compression logic to a common > > location, as the latter is an independent improvement to the existing > > codebase. Additionally, since the test file refactoring was only kept > > separate to facilitate the review and has already been reviewed, I > > suggest merging those changes into the main feature patch i.e. 0002. > > All other elements should remain in a single preparatory refactoring > > patch for pg_waldump. > > > > Attached is the version that includes the proposed split. No > > additional changes to 0002 and 0003 patches. > > > > Added the two missing 'Reviewed-by' lines to the credit section of the > commit message and did a minor optimization in get_archive_wal_entry. > Attaching an updated version. It includes some tweaks to code comments, adds an assert inside get_archive_wal_entry(), moves the archive_read_buf_size declaration and usage into an assert-enabled check, and makes a minor change to precheck_tar_backup_file() to assign out-variables only after successful validation. Regards, Amul
-
Re: pg_waldump: support decoding of WAL inside tarfile
Zsolt Parragi <zsolt.parragi@percona.com> — 2026-03-19T20:48:27Z
Hello! Path is ignored with a positional argument, I think this is a bug? This fails: pg_waldump --path /wal/dir 000000010000000000000001 And this works: pg_waldump --path /wal/dir --start 0/01000028 --end 0/010020F8 +{ + int fname_len = strlen(fname); + Shouldn't this use size_t? + /* + * Setup temporary directory to store WAL segments and set up an exit + * callback to remove it upon completion. + */ + setup_tmpwal_dir(waldir); Maybe this could be deferred to be created only on first use? If I understand correctly, in a typical scenario waldump won't use this temporary directory, yet it always creates it. -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-03-20T11:31:00Z
On Fri, Mar 20, 2026 at 2:18 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote: > > Hello! > > Path is ignored with a positional argument, I think this is a bug? > > This fails: > > pg_waldump --path /wal/dir 000000010000000000000001 > > And this works: > > pg_waldump --path /wal/dir --start 0/01000028 --end 0/010020F8 > Good catch! I've fixed this in the attached version and updated a test case to cover this scenario. > +{ > + int fname_len = strlen(fname); > + > > Shouldn't this use size_t? > Okay, that can be used. I’ve done the same in the attached version. > + /* > + * Setup temporary directory to store WAL segments and set up an exit > + * callback to remove it upon completion. > + */ > + setup_tmpwal_dir(waldir); > > Maybe this could be deferred to be created only on first use? If I > understand correctly, in a typical scenario waldump won't use this > temporary directory, yet it always creates it. Yeah, that optimization can be done, but passing the waldir -- which is only used once -- to the point where the first temp file is created would require quite a bit of code refactoring that doesn't seem to offer much gain, IMO. Regards, Amul -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-03-20T13:26:42Z
On Fri, Mar 20, 2026 at 5:01 PM Amul Sul <sulamul@gmail.com> wrote: > > On Fri, Mar 20, 2026 at 2:18 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote: > > > > Hello! > > > > Path is ignored with a positional argument, I think this is a bug? > > > > This fails: > > > > pg_waldump --path /wal/dir 000000010000000000000001 > > > > And this works: > > > > pg_waldump --path /wal/dir --start 0/01000028 --end 0/010020F8 > > > > Good catch! I've fixed this in the attached version and updated a test > case to cover this scenario. > > > +{ > > + int fname_len = strlen(fname); > > + > > > > Shouldn't this use size_t? > > > > Okay, that can be used. I’ve done the same in the attached version. > > > + /* > > + * Setup temporary directory to store WAL segments and set up an exit > > + * callback to remove it upon completion. > > + */ > > + setup_tmpwal_dir(waldir); > > > > Maybe this could be deferred to be created only on first use? If I > > understand correctly, in a typical scenario waldump won't use this > > temporary directory, yet it always creates it. > > Yeah, that optimization can be done, but passing the waldir -- which > is only used once -- to the point where the first temp file is created > would require quite a bit of code refactoring that doesn't seem to > offer much gain, IMO. > Since Andrew also leans toward creating the directory only when needed, I have reconsidered the approach. I think we can pass waldir (the archive directory) via XLogDumpPrivate, and I’ve implemented that in the attached version. Regards, Amul -
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-03-20T19:33:18Z
On 2026-03-20 Fr 9:26 AM, Amul Sul wrote: > On Fri, Mar 20, 2026 at 5:01 PM Amul Sul <sulamul@gmail.com> wrote: >> On Fri, Mar 20, 2026 at 2:18 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote: >>> Hello! >>> >>> Path is ignored with a positional argument, I think this is a bug? >>> >>> This fails: >>> >>> pg_waldump --path /wal/dir 000000010000000000000001 >>> >>> And this works: >>> >>> pg_waldump --path /wal/dir --start 0/01000028 --end 0/010020F8 >>> >> Good catch! I've fixed this in the attached version and updated a test >> case to cover this scenario. >> >>> +{ >>> + int fname_len = strlen(fname); >>> + >>> >>> Shouldn't this use size_t? >>> >> Okay, that can be used. I’ve done the same in the attached version. >> >>> + /* >>> + * Setup temporary directory to store WAL segments and set up an exit >>> + * callback to remove it upon completion. >>> + */ >>> + setup_tmpwal_dir(waldir); >>> >>> Maybe this could be deferred to be created only on first use? If I >>> understand correctly, in a typical scenario waldump won't use this >>> temporary directory, yet it always creates it. >> Yeah, that optimization can be done, but passing the waldir -- which >> is only used once -- to the point where the first temp file is created >> would require quite a bit of code refactoring that doesn't seem to >> offer much gain, IMO. >> > Since Andrew also leans toward creating the directory only when > needed, I have reconsidered the approach. I think we can pass waldir > (the archive directory) via XLogDumpPrivate, and I’ve implemented that > in the attached version. > Thanks, committed with very minor tweaks. cheers andrew -- Andrew Dunstan EDB: https://www.enterprisedb.com -
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-21T03:49:02Z
Andrew Dunstan <andrew@dunslane.net> writes: > Thanks, committed with very minor tweaks. Buildfarm members batta and hachi don't like this very much. They fail the pg_verifybackup tests like so: # Running: pg_verifybackup --exit-on-error /home/admin/batta/buildroot/HEAD/pgsql.build/src/bin/pg_verifybackup/tmp_check/t_008_untar_primary_data/backup/server-backup pg_waldump: error: could not find WAL in archive "base.tar.zst" pg_verifybackup: error: WAL parsing failed for timeline 1 Only the zstd-compression case fails. I've spent several hours trying to reproduce this, without any luck, although I can get a similar failure in only the gzip case if I build with --with-wal-blocksize=64. I do not have an explanation for the seeming cross-platform difference. However after adding a lot of debug tracing, I believe I see the bug, or at least a related bug. This bit in archive_waldump.c's init_archive_reader is where the error comes from: /* * Read until we have at least one full WAL page (XLOG_BLCKSZ bytes) from * the first WAL segment in the archive so we can extract the WAL segment * size from the long page header. */ while (entry == NULL || entry->buf->len < XLOG_BLCKSZ) { if (read_archive_file(privateInfo, XLOG_BLCKSZ) == 0) pg_fatal("could not find WAL in archive \"%s\"", privateInfo->archive_name); entry = privateInfo->cur_file; } That looks plausible but is in fact utterly broken when there's not a lot of WAL data in the archive, as there is not in this test case. There are at least two problems: 1. read_archive_file reads some data from the source WAL archive and shoves it into the astreamer decompression pipeline. However, once it runs out of source data, it just returns zero and we fail immediately. This does not account for the possibility --- nay, certainty --- that there is data queued inside the decompression pipeline. So this doesn't work if the data we need has been compressed into less than XLOG_BLCKSZ worth of compressed data. (I suppose that the seeming cross-platform differences have to do with the effectiveness of the compression algorithm, but I don't really understand why it'd not be the same everywhere.) We need to do astreamer_finalize once we run out of source data. I think the cleanest place to handle that would be inside read_archive_file, but its return convention will need some rework if we want to put it there (because rc == 0 shouldn't cause an immediate failure if we were able to finalize some more data). As an ugly experiment I put an astreamer_finalize call into the rc == 0 path of the above loop, but it still didn't work, because: 2. If the decompression pipeline reaches the end of the WAL file that we want, the ASTREAMER_MEMBER_TRAILER case in astreamer_waldump_content instantly resets privateInfo->cur_file to NULL. Then the loop in init_archive_reader cannot exit successfully, and it will just read till the end of the archive and fail. I see that of the three callers of read_archive_file, only get_archive_wal_entry is aware of this possibility; but init_archive_reader certainly needs to deal with it and I bet read_archive_wal_page does too. Moreover, get_archive_wal_entry's solution looks to me like a fragile kluge that probably doesn't work reliably either, the reason being that privateInfo->cur_file can change multiple times during a single call to read_archive_file, if the WAL data has been compressed sufficiently. That whole API seems to need some rethinking, not to mention better documentation than the zero it has now. While I'm bitching: this error message "could not find WAL in archive \"%s\"" seems to me to be completely misleading and off-point. regards, tom lane -
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-03-21T06:19:28Z
On Sat, Mar 21, 2026 at 9:19 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: > > Andrew Dunstan <andrew@dunslane.net> writes: > > Thanks, committed with very minor tweaks. > > Buildfarm members batta and hachi don't like this very much. > They fail the pg_verifybackup tests like so: > > # Running: pg_verifybackup --exit-on-error /home/admin/batta/buildroot/HEAD/pgsql.build/src/bin/pg_verifybackup/tmp_check/t_008_untar_primary_data/backup/server-backup > pg_waldump: error: could not find WAL in archive "base.tar.zst" > pg_verifybackup: error: WAL parsing failed for timeline 1 > > Only the zstd-compression case fails. I've spent several hours trying > to reproduce this, without any luck, although I can get a similar > failure in only the gzip case if I build with --with-wal-blocksize=64. > I do not have an explanation for the seeming cross-platform > difference. However after adding a lot of debug tracing, I believe > I see the bug, or at least a related bug. This bit in > archive_waldump.c's init_archive_reader is where the error comes from: > > /* > * Read until we have at least one full WAL page (XLOG_BLCKSZ bytes) from > * the first WAL segment in the archive so we can extract the WAL segment > * size from the long page header. > */ > while (entry == NULL || entry->buf->len < XLOG_BLCKSZ) > { > if (read_archive_file(privateInfo, XLOG_BLCKSZ) == 0) > pg_fatal("could not find WAL in archive \"%s\"", > privateInfo->archive_name); > > entry = privateInfo->cur_file; > } > > That looks plausible but is in fact utterly broken when there's not a > lot of WAL data in the archive, as there is not in this test case. > There are at least two problems: > Thanks for the detailed debugging. I noticed the failure this morning and had started investigating the issue, but in the meantime, I got your helpful reply, which saved me a bunch of time and energy. > 1. read_archive_file reads some data from the source WAL archive and > shoves it into the astreamer decompression pipeline. However, once it > runs out of source data, it just returns zero and we fail immediately. > This does not account for the possibility --- nay, certainty --- that > there is data queued inside the decompression pipeline. So this > doesn't work if the data we need has been compressed into less than > XLOG_BLCKSZ worth of compressed data. (I suppose that the seeming > cross-platform differences have to do with the effectiveness of the > compression algorithm, but I don't really understand why it'd not be > the same everywhere.) We need to do astreamer_finalize once we run > out of source data. I think the cleanest place to handle that would > be inside read_archive_file, but its return convention will need some > rework if we want to put it there (because rc == 0 shouldn't cause an > immediate failure if we were able to finalize some more data). As an > ugly experiment I put an astreamer_finalize call into the rc == 0 path > of the above loop, but it still didn't work, because: > > 2. If the decompression pipeline reaches the end of the WAL file that > we want, the ASTREAMER_MEMBER_TRAILER case in > astreamer_waldump_content instantly resets privateInfo->cur_file to > NULL. Then the loop in init_archive_reader cannot exit successfully, > and it will just read till the end of the archive and fail. > > I see that of the three callers of read_archive_file, only > get_archive_wal_entry is aware of this possibility; but > init_archive_reader certainly needs to deal with it and I bet > read_archive_wal_page does too. Moreover, get_archive_wal_entry's > solution looks to me like a fragile kluge that probably doesn't work > reliably either, the reason being that privateInfo->cur_file can > change multiple times during a single call to read_archive_file, > if the WAL data has been compressed sufficiently. That whole API > seems to need some rethinking, not to mention better documentation > than the zero it has now. > I agree; init_archive_reader needs that handling, but read_archive_wal_page doesn't need any fix. Since it only deals with the current entry and already holds a reference to it, there is no need to fetch it from the hash table again. init_archive_reader has to scan the hash table because it doesn't already have the specific WAL filename it is looking for, unlike get_archive_wal_entry. Please have a look at the attached patch, which tries to fix that. > While I'm bitching: this error message "could not find WAL in archive > \"%s\"" seems to me to be completely misleading and off-point. > I tried to improve that in the attached version. regards, Amul -
Re: pg_waldump: support decoding of WAL inside tarfile
Michael Paquier <michael@paquier.xyz> — 2026-03-21T06:23:41Z
On Fri, Mar 20, 2026 at 11:49:02PM -0400, Tom Lane wrote: > Andrew Dunstan <andrew@dunslane.net> writes: > > Thanks, committed with very minor tweaks. > > Buildfarm members batta and hachi don't like this very much. > They fail the pg_verifybackup tests like so: > > # Running: pg_verifybackup --exit-on-error /home/admin/batta/buildroot/HEAD/pgsql.build/src/bin/pg_verifybackup/tmp_check/t_008_untar_primary_data/backup/server-backup > pg_waldump: error: could not find WAL in archive "base.tar.zst" > pg_verifybackup: error: WAL parsing failed for timeline 1 I did not look at what's happening on the host, but it seems like a safe bet to assume that we are not seeing many failures in the buildfarm because we don't have many animals that have the idea to add --with-zstd to their build configuration, like these two ones. -- Michael
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-21T06:34:00Z
Michael Paquier <michael@paquier.xyz> writes: > On Fri, Mar 20, 2026 at 11:49:02PM -0400, Tom Lane wrote: >> Buildfarm members batta and hachi don't like this very much. > I did not look at what's happening on the host, but it seems like a > safe bet to assume that we are not seeing many failures in the > buildfarm because we don't have many animals that have the idea to add > --with-zstd to their build configuration, like these two ones. That may be part of the story, but only part. I spent a good deal of time trying to reproduce batta & hachi's configurations locally, on several different platforms, but still couldn't duplicate what they are showing. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-03-21T12:21:49Z
On 2026-03-21 Sa 2:34 AM, Tom Lane wrote: > Michael Paquier<michael@paquier.xyz> writes: >> On Fri, Mar 20, 2026 at 11:49:02PM -0400, Tom Lane wrote: >>> Buildfarm members batta and hachi don't like this very much. >> I did not look at what's happening on the host, but it seems like a >> safe bet to assume that we are not seeing many failures in the >> buildfarm because we don't have many animals that have the idea to add >> --with-zstd to their build configuration, like these two ones. > That may be part of the story, but only part. I spent a good deal of > time trying to reproduce batta & hachi's configurations locally, on > several different platforms, but still couldn't duplicate what they > are showing. > > Yeah, I haven't been able to reproduce it either. But while investigating I found a couple of issues. We neglected to add one of the tests to meson.build, and we neglected to close some files, causing errors on windows. I also noticed a possible bug in astreamer, where the decompressor finalize functions send bbs_buffer.maxlen bytes to the next streamer when flushing remaining data at end-of-stream. This seems wrong because the buffer may only be partially filled with valid decompressed data. Possible patch for that attached. (But I don't think it's related to these failures). cheers andrew -- Andrew Dunstan EDB:https://www.enterprisedb.com
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-21T14:45:46Z
Andrew Dunstan <andrew@dunslane.net> writes: > I also noticed a possible bug in astreamer, where the decompressor > finalize functions send bbs_buffer.maxlen bytes to the next streamer > when flushing remaining data at end-of-stream. This seems wrong because > the buffer may only be partially filled with valid decompressed data. > Possible patch for that attached. (But I don't think it's related to > these failures). Ugh. That's surely very broken, but how did we not notice? Do all the consumers know enough to ignore garbage trailing data? regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-03-21T15:35:50Z
On Sat, Mar 21, 2026 at 5:51 PM Andrew Dunstan <andrew@dunslane.net> wrote: > > > On 2026-03-21 Sa 2:34 AM, Tom Lane wrote: > > Michael Paquier <michael@paquier.xyz> writes: > > On Fri, Mar 20, 2026 at 11:49:02PM -0400, Tom Lane wrote: > > Buildfarm members batta and hachi don't like this very much. > > I did not look at what's happening on the host, but it seems like a > safe bet to assume that we are not seeing many failures in the > buildfarm because we don't have many animals that have the idea to add > --with-zstd to their build configuration, like these two ones. > > That may be part of the story, but only part. I spent a good deal of > time trying to reproduce batta & hachi's configurations locally, on > several different platforms, but still couldn't duplicate what they > are showing. > > > > > > Yeah, I haven't been able to reproduce it either. But while investigating I found a couple of issues. We neglected to add one of the tests to meson.build, and we neglected to close some files, causing errors on windows. > While the proposed fix of closing the file pointer before returning is correct, we also need to ensure the file is reopened in the next call to spill any remaining buffered data. I’ve made a small update to Andrew's 0001 patch to handle this. Also, changes to meson.build don't seem to be needed as we haven't committed that file yet (unless I am missing something). I’ve also reattached the other patches so they don't get lost: v2-0002 is Andrew's patch for the archive streamer, and v2-0003 is the patch I posted previously [1]. Regards, Amul 1] http://postgr.es/m/CAAJ_b95L5J7bjRNDjRj6WgqFcQeaBD+JX3sAuxPA4uopqEThxA@mail.gmail.com
-
Re: pg_waldump: support decoding of WAL inside tarfile
amul sul <sulamul@gmail.com> — 2026-03-21T17:26:38Z
On Sat, Mar 21, 2026 at 9:05 PM Amul Sul <sulamul@gmail.com> wrote: > > On Sat, Mar 21, 2026 at 5:51 PM Andrew Dunstan <andrew@dunslane.net> wrote: > > > > > > On 2026-03-21 Sa 2:34 AM, Tom Lane wrote: > > > > Michael Paquier <michael@paquier.xyz> writes: > > > > On Fri, Mar 20, 2026 at 11:49:02PM -0400, Tom Lane wrote: > > > > Buildfarm members batta and hachi don't like this very much. > > > > I did not look at what's happening on the host, but it seems like a > > safe bet to assume that we are not seeing many failures in the > > buildfarm because we don't have many animals that have the idea to add > > --with-zstd to their build configuration, like these two ones. > > > > That may be part of the story, but only part. I spent a good deal of > > time trying to reproduce batta & hachi's configurations locally, on > > several different platforms, but still couldn't duplicate what they > > are showing. > > > > > > > > > > > > Yeah, I haven't been able to reproduce it either. But while investigating I found a couple of issues. We neglected to add one of the tests to meson.build, and we neglected to close some files, causing errors on windows. > > > > While the proposed fix of closing the file pointer before returning is > correct, we also need to ensure the file is reopened in the next call > to spill any remaining buffered data. I’ve made a small update to > Andrew's 0001 patch to handle this. Also, changes to meson.build don't > seem to be needed as we haven't committed that file yet (unless I am > missing something). > > I’ve also reattached the other patches so they don't get lost: v2-0002 > is Andrew's patch for the archive streamer, and v2-0003 is the patch I > posted previously [1]. > > On further thought, I don't think v2-0001 is the right patch. Consider the case where we write a temporary file partially: if the next segment required for decoding is that same segment, TarWALDumpReadPage() will find the physical file present and continue decoding, potentially triggering an error later due to the shorter file. I have attached the v3-0001 patch, which ensures that once we start writing a temporary file, it should be finished before performing the lookup. This ensures we don't leave a partial file on disk. Updated patches are attached; 0002 and 0003 remain the same as before. Regards, Amul
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-21T18:23:55Z
I have made some progress on the question of how to reproduce these failures. If I do this: diff --git a/src/bin/pg_waldump/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c index b078c2d6960..c389a227be5 100644 --- a/src/bin/pg_waldump/archive_waldump.c +++ b/src/bin/pg_waldump/archive_waldump.c @@ -178,7 +178,7 @@ init_archive_reader(XLogDumpPrivate *privateInfo, */ while (entry == NULL || entry->buf->len < XLOG_BLCKSZ) { - if (read_archive_file(privateInfo, XLOG_BLCKSZ) == 0) + if (read_archive_file(privateInfo, READ_CHUNK_SIZE) == 0) pg_fatal("could not find WAL in archive \"%s\"", privateInfo->archive_name); then I get the "could not find WAL in archive" failures in pg_verifybackup's gzip and lz4 tests, but not zstd. This happens reproducibly even without any special hacks on XLOG_BLCKSZ or wal_compression settings. Of course, this is not exactly what's happening on batta/hachi, because they fail on zstd and not the other two. But I think it confirms my theory that the problem is essentially poor handling of EOF boundary conditions. (Per discussion, there are other bugs here too; I don't mean to minimize that aspect.) regards, tom lane -
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-21T19:31:10Z
I don't like the v3 patches too much: in particular, they do nothing for the failure-to-finalize bug I identified yesterday. v3-0003 is on the right track but seems overcomplicated. Here is my own set of proposed patches, which I think will fix what we are seeing in the buildfarm: v4-0001 is Andrew's fix for incorrect decompressor finalization. v4-0002 fixes read_archive_file() to do the missing finalize step. v4-0003 fixes init_archive_reader() to not depend on cur_file. This is closely allied to v3-0003 but simpler. I also added some commentary to pg_waldump.h about what it's safe to do with cur_file. get_archive_wal_entry() violates that advice and is pretty much utterly broken IMO, because it still believes that it can use cur_file in an incorrect way. However, the impact of that is that it may fail to flush some hashtable entries out to temp files (in case a single read_archive_file() step reads more than one WAL file, which is entirely possible with compression). That is a performance issue but it's not causing our buildfarm problems, so I left it untouched here. But I don't think any of the patches proposed so far fix it properly. What it should do should look more like the revised version of init_archive_reader's loop: call read_archive_file(), then scan the hash table for WAL entries we need to flush to files, then finally return the desired WAL entry if it's present, else loop around. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-21T22:24:07Z
I wrote: > Andrew Dunstan <andrew@dunslane.net> writes: >> I also noticed a possible bug in astreamer, where the decompressor >> finalize functions send bbs_buffer.maxlen bytes to the next streamer >> when flushing remaining data at end-of-stream. This seems wrong because >> the buffer may only be partially filled with valid decompressed data. >> Possible patch for that attached. (But I don't think it's related to >> these failures). > Ugh. That's surely very broken, but how did we not notice? > Do all the consumers know enough to ignore garbage trailing data? I poked into this question and found that all of our existing uses of the decompression astreamers are in front of astreamer_tar.c's astreamer_tar_parser_ops. Since a tar file has a trailer (two zero blocks), it is not surprising that garbage after the trailer will be ignored. It might appear that astreamer_tar_parser_content itself will complain about such extra data: case ASTREAMER_ARCHIVE_TRAILER: /* * We've seen an end-of-archive indicator, so anything more is * buffered and sent as part of the archive trailer. But we * don't expect more than 2 blocks. */ astreamer_buffer_bytes(streamer, &data, &len, len); if (len > 2 * TAR_BLOCK_SIZE) pg_fatal("tar file trailer exceeds 2 blocks"); return; But this code is itself buggy! The astreamer_buffer_bytes call is guaranteed to reduce "len" to zero, therefore that pg_fatal call can never fire, no matter how much garbage we just stuffed into the astreamer's buffer. We might want to do - if (len > 2 * TAR_BLOCK_SIZE) + if (streamer->bbs_buffer.len > 2 * TAR_BLOCK_SIZE) Unsurprisingly, applying this change to unmodified master results in the pg_waldump and pg_verifybackup tests falling over. More surprisingly, they still fall over after applying your fix to the decompressors, so there's some other source of garbage trailing data. I haven't figured out what. However, I'm a bit loath to make that change even after fixing the decompressors, because this discovery suggests that we may currently be building tar files that contain garbage after the trailer. And it seems to me that such tar files are perfectly legal anyway. The POSIX spec for "ustar" format (found under pax(1)) says At the end of the archive file there shall be two 512-octet logical records filled with binary zeros, interpreted as an end-of-archive indicator. The logical records may be grouped for physical I/O operations, as described under the -bblocksize and -x ustar options. Each group of logical records may be written with a single operation equivalent to the write() function. On magnetic tape, the result of this write shall be a single tape physical block. The last physical block shall always be the full size, so logical records after the two zero logical records may contain undefined data. That last sentence seems to say that you can put arbitrary garbage after the trailer without invalidating the file. Sure, it's a holdover from magtape days, but that's how the spec is written. Therefore, I think we might be best off to just remove this nonfunctional error check, rather than "fix" it. Another idea which might be a bit better is to drop any data after the standard trailer on the floor, instead of passing it on as the code now does. Thoughts? regards, tom lane -
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-22T04:24:47Z
I wrote: > Unsurprisingly, applying this change to unmodified master results > in the pg_waldump and pg_verifybackup tests falling over. More > surprisingly, they still fall over after applying your fix to the > decompressors, so there's some other source of garbage trailing > data. I haven't figured out what. In the learn-something-new-every-day dept.: good ol' GNU tar itself does that. By default, it zero-pads its output to a multiple of 10kB after it's written the required terminator. Moreover, this behavior is actually specified by POSIX: -x format Specify the output archive format. The pax utility shall support the following formats: ... ustar The tar interchange format; see the EXTENDED DESCRIPTION section. The default blocksize for this format for character special archive files shall be 10240. Implementations shall support all blocksize values less than or equal to 32256 that are multiples of 512. So, astreamer_tar_parser_content's idea that it should disallow more than 1024 bytes of trailer is completely wrong, which we would have figured out long ago if the code attempting to enforce that weren't completely broken. You could argue that this means the tar files our existing utilities create aren't POSIX-compliant. I think it's all right though: we can just say that we write these files with blocksize 1024 not blocksize 10240, and tar-file readers are required to accept that per the above spec text. However, this discourages me from editorializing on the file trailer emitted by whatever wrote the tar file we are reading. I think emitting it as-is is the most appropriate thing. So we should just get rid of astreamer_tar_parser_content's nonfunctional error check and not change its behavior otherwise. regards, tom lane -
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-03-22T11:24:56Z
On Sun, Mar 22, 2026 at 12:24 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: > I wrote: > > Unsurprisingly, applying this change to unmodified master results > > in the pg_waldump and pg_verifybackup tests falling over. More > > surprisingly, they still fall over after applying your fix to the > > decompressors, so there's some other source of garbage trailing > > data. I haven't figured out what. > > In the learn-something-new-every-day dept.: good ol' GNU tar itself > does that. By default, it zero-pads its output to a multiple of 10kB > after it's written the required terminator. Moreover, this behavior > is actually specified by POSIX: > > -x format > Specify the output archive format. The pax utility shall support > the following formats: > ... > ustar > The tar interchange format; see the EXTENDED DESCRIPTION > section. The default blocksize for this format for character > special archive files shall be 10240. Implementations shall > support all blocksize values less than or equal to 32256 that > are multiples of 512. > > So, astreamer_tar_parser_content's idea that it should disallow more > than 1024 bytes of trailer is completely wrong, which we would have > figured out long ago if the code attempting to enforce that weren't > completely broken. > > You could argue that this means the tar files our existing utilities > create aren't POSIX-compliant. I think it's all right though: we > can just say that we write these files with blocksize 1024 not > blocksize 10240, and tar-file readers are required to accept that > per the above spec text. > > However, this discourages me from editorializing on the file trailer > emitted by whatever wrote the tar file we are reading. I think > emitting it as-is is the most appropriate thing. So we should just > get rid of astreamer_tar_parser_content's nonfunctional error check > and not change its behavior otherwise. > > > OK, patch 5 of this set does that. I reworked your previous patches 2 and 3 slightly - mostly additional comments, and fixing a bug in use of sizeof(XLogLongPageHeader). Patch 4 here tries to fix the wrong use of cur_file in get_archive_wal_entry() cheers andrew
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-22T17:29:44Z
Andrew Dunstan <andrew@dunslane.net> writes: > OK, patch 5 of this set does that. I reworked your previous patches 2 and 3 > slightly - mostly additional comments, and fixing a bug in use > of sizeof(XLogLongPageHeader). Patch 4 here tries to fix the wrong use of > cur_file in get_archive_wal_entry() A few nits: 0001: I think we need to rewrite the commit message now that we've run the unknowns to ground. Perhaps like Send the correct amount of data to the next astreamer, not the whole allocated buffer size. This bug escaped detection because in present uses the next astreamer is always a tar-file parser which is insensitive to trailing garbage. But that may not always be true. Also, I think we ought to back-patch 0001, just in case we have misunderstood the scope of usage or somebody tries to make use of back-branch code for a new purpose. This is a bit tedious because before f80b09bac/3c9056981 the code was somewhere else and used different names. But the bugs are clearly there, back to v15. 0002, 0003, 0005: LGTM. 0004: Don't like this at all. It fails to respond to the point I was trying to make: the code will only consider spilling hash entries that are the cur_file at some read_archive_file boundary. Also, it seems to be doubling down on the already over-complicated and probably under-correct goal of spilling entries that are still in progress of being read. We can make this function far simpler and more obviously correct if we just accept that we'll read a WAL file completely before spilling it. See my proposed alternative to 0004, attached. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-22T18:17:53Z
I wrote: > ... We can make this function far simpler > and more obviously correct if we just accept that we'll read a > WAL file completely before spilling it. See my proposed > alternative to 0004, attached. Actually, we can make that better yet by not expecting get_archive_wal_entry to clean up after init_archive's failure to free all irrelevant hashtable entries. Better version attached. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-03-22T21:19:09Z
On Sun, Mar 22, 2026 at 2:17 PM Tom Lane <tgl@sss.pgh.pa.us> wrote: > I wrote: > > ... We can make this function far simpler > > and more obviously correct if we just accept that we'll read a > > WAL file completely before spilling it. See my proposed > > alternative to 0004, attached. > > Actually, we can make that better yet by not expecting > get_archive_wal_entry to clean up after init_archive's > failure to free all irrelevant hashtable entries. > Better version attached. > > > Yeah, this looks good. I know we also still need to do something about rmtree trying to remove files we haven't closed. But what we have so far in this set LGTM. If you want to push this I'm good, otherwise I'll look at it tomorrow or Tuesday. cheers andrew
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-22T21:39:26Z
Andrew Dunstan <andrew@dunslane.net> writes: > Yeah, this looks good. I know we also still need to do something about > rmtree trying to remove files we haven't closed. But what we have so far in > this set LGTM. If you want to push this I'm good, otherwise I'll look at it > tomorrow or Tuesday. WFM. I'd like to get the buildfarm green again, so I'll work on pushing this stuff now. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-22T23:13:11Z
Everything's pushed, although it looks like it'll be a few hours before we see results from batta or hachi. In the meantime, I have one more change that I'd like to propose in archive_waldump.c: I think we should drop read_archive_file's "count" parameter and just always try to fill archive_read_buf completely, as attached. I don't believe that using a variable count is buying anything. I get that the idea was to not load more data than we have to before init_archive_reader can identify the segment size and start filtering data we don't need. But I doubt that that helps a whole lot: I think most of the cost in this pipeline is going into decompression and tar format parsing, not into copying data into the hashtable. Also, I noticed while I was investigating the bugs we just fixed that the tar file very likely contains a ton of other data before the first WAL file. In the regression test scenarios it seems to reliably have all of the cluster's actual table data ahead of the WAL directory. By using a small read count, we are slowing down scanning of all that data. Maybe not by much, I've not tried to measure it. But I think the case for using a variable count is very weak. Even if you don't like that change, I think we should apply the parts of the attached that change archive_read_buf_size into an always-present, always-filled field of XLogDumpPrivate. Having its presence be dependent on USE_ASSERT_CHECKING is just asking for trouble, ie different .c files having a different opinion of whether it's there. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-23T03:02:20Z
I wrote: > Everything's pushed, although it looks like it'll be a few hours > before we see results from batta or hachi. batta just went green, so I'm feeling optimistic that we've correctly diagnosed and fixed the problem. Meanwhile, I noticed one more thing that could use fixing: while astreamer_zstd.c is careful to use a reasonably-sized output bbs_buffer (about 256kB looks like), the decompressors in astreamer_gzip.c and astreamer_lz4.c just leave bbs_buffer's size at the default 1kB. This means we're pushing decompressed data to the next astreamer in very small units in those cases. I suspect strongly that this difference in buffer sizes has a lot to do with the observed results that only zstd failed on batta/hachi while only the other two failed in the test cases that I've been able to reproduce here. I'm too lazy to try to run that to ground though, especially since it's mostly moot now. But I think that on performance grounds, we ought to use a reasonable buffer size for all three decompressors. I also noticed that astreamer_zstd.c was using ZSTD_DStreamOutSize() as the target buffer size for both compression and decompression, although the libzstd API provides a separate function ZSTD_CStreamOutSize() for the compression case. Those two functions produce the same result (256K) on my machine, so this seems to be just a cosmetic issue, but it still seems pretty tin-eared to not use the functions as specified. Proposed patch attached. There might be an argument for using some other size than 256K for the other two decompressors, but my inclination is to try to make all three use roughly the same block size. (See also 66ec01dc4.) regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Michael Paquier <michael@paquier.xyz> — 2026-03-24T03:11:44Z
On Sun, Mar 22, 2026 at 11:02:20PM -0400, Tom Lane wrote: > Proposed patch attached. There might be an argument for using some > other size than 256K for the other two decompressors, but my > inclination is to try to make all three use roughly the same block > size. (See also 66ec01dc4.) The buildfarm has switched mostly to green, except on this one: https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=hoatzin&dt=2026-03-23%2006%3A00%3A42 -- Michael
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-25T17:25:21Z
Michael Paquier <michael@paquier.xyz> writes: > The buildfarm has switched mostly to green, except on this one: > https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=hoatzin&dt=2026-03-23%2006%3A00%3A42 Yeah, there are some other weird failures on other machines too. There's also the problem we already knew about of FD leakage breaking cleanup of the temp file directory on Windows. I wrote a patch to fix the FD leakage problem (v8-0001 attached). I don't have Windows at hand, but I tested it by dint of having the atexit callback invoke "lsof" to see if there were any open files in the temp directory. I also occasionally saw some of the weird errors mentioned above. After much debugging, I believe that the issue is that archive_waldump.c is unaware that inserting or deleting entries in a simplehash.h hash table can cause other entries to move. That can break privateInfo->cur_file, and it can also break read_archive_wal_page which thought it could just re-use its entry pointer after calling read_archive_file. v8-0002 attached fixes that, and I'm not seeing weird failures anymore. Two additional thoughts: 1. The amount of data that pg_waldump's TAP tests use is not sufficient to trigger these problems with any degree of reliability. I'm hesitant to make the tests run longer, but clearly we do not have adequate coverage now. 2. I didn't do it here, but I urgently think we should rip out read_archive_wal_page's stanza that truncates the entry's "buf" string (the "if (privateInfo->decoding_started)" part). My faith in this code in general is at rock bottom, and my faith in the extent to which we've tested it is somewhere below ground level. I don't think we need rickety optimizations that serve only to keep the active hashtable entry to less than 16MB, when we're going to reclaim that space altogether as soon as we've finished dumping that segment. This truncation scares me because it adds a whole 'nother level of poorly-documented complexity to the invariants around what is in entry->buf. Also, while we theoretically should not need to spill the entry after this point, if we did we would write a corrupted spill file. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Andres Freund <andres@anarazel.de> — 2026-03-25T17:28:26Z
Hi, On 2026-03-24 12:11:44 +0900, Michael Paquier wrote: > On Sun, Mar 22, 2026 at 11:02:20PM -0400, Tom Lane wrote: > > Proposed patch attached. There might be an argument for using some > > other size than 256K for the other two decompressors, but my > > inclination is to try to make all three use roughly the same block > > size. (See also 66ec01dc4.) > > The buildfarm has switched mostly to green, except on this one: > https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=hoatzin&dt=2026-03-23%2006%3A00%3A42 I think there's a few more failues. Fairywren regularly fails, including in a run from today. https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&dt=2026-03-25%2003%3A48%3A06 There also are a lot of CI failures. E.g. https://cirrus-ci.com/task/6153854431002624 https://api.cirrus-ci.com/v1/artifact/task/6153854431002624/testrun/build/testrun/pg_waldump/001_basic/log/regress_log_001_basic # Running: pg_waldump --path C:\msys64\tmp\tNfU5IfQ4a/pg_wal.tar.gz --start 0/01806F48 --end 0/03093BD8 [22:46:25.358](3.991s) not ok 160 - runs with path option and start and end locations: exit code 0 [22:46:25.363](0.005s) # Failed test 'runs with path option and start and end locations: exit code 0' # at C:/cirrus/src/bin/pg_waldump/t/001_basic.pl line 399. [22:46:25.364](0.001s) ok 161 - runs with path option and start and end locations: no stderr [22:46:25.365](0.001s) not ok 162 - runs with path option and start and end locations: matches [22:46:25.365](0.000s) # Failed test 'runs with path option and start and end locations: matches' # at C:/cirrus/src/bin/pg_waldump/t/001_basic.pl line 399. [22:46:25.366](0.000s) # '' # doesn't match '(?^:.)' I was first suspecting that this is due to commit 1c162c965a1 Author: Fujii Masao <fujii@postgresql.org> Date: 2026-03-24 22:33:09 +0900 Report detailed errors from XLogFindNextRecord() failures. but there are afaict failures from before that: https://cirrus-ci.com/task/5501609960013824 which is for 4019f725f5d, preceding 1c162c965a1 and https://cirrus-ci.com/task/5317196043255808 It does feel however the failure frequency has increased substantially: https://cirrus-ci.com/github/postgres/postgres/master Greetings, Andres Freund -
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-03-28T21:36:06Z
On Thu, Mar 26, 2026 at 6:28 AM Andres Freund <andres@anarazel.de> wrote: > On 2026-03-24 12:11:44 +0900, Michael Paquier wrote: > > On Sun, Mar 22, 2026 at 11:02:20PM -0400, Tom Lane wrote: > > > Proposed patch attached. There might be an argument for using some > > > other size than 256K for the other two decompressors, but my > > > inclination is to try to make all three use roughly the same block > > > size. (See also 66ec01dc4.) > > > > The buildfarm has switched mostly to green, except on this one: > > https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=hoatzin&dt=2026-03-23%2006%3A00%3A42 > > I think there's a few more failues. Fairywren regularly fails, including in a > run from today. This fails 100% of the time on my machine, even after e9d72348 and ff84efe4, eg: # Running: pg_waldump --path /tmp/D8WG1Sv2HE/pg_wal.tar --start 0/017A2610 --end 0/02093848 [09:43:29.288](0.148s) not ok 104 - runs with path option and start and end locations: exit code 0 [09:43:29.289](0.001s) # Failed test 'runs with path option and start and end locations: exit code 0' # at /home/tmunro/projects/postgresql/src/bin/pg_waldump/t/001_basic.pl line 402. [09:43:29.290](0.001s) not ok 105 - runs with path option and start and end locations: no stderr [09:43:29.291](0.001s) # Failed test 'runs with path option and start and end locations: no stderr' # at /home/tmunro/projects/postgresql/src/bin/pg_waldump/t/001_basic.pl line 402. [09:43:29.291](0.000s) # got: 'pg_waldump: error: could not find WAL "000000010000000000000002" in archive "pg_wal.tar" # ' I can see that it is wrong about the contents of the tar file: $ pg_waldump --path _tmp_H_1gv81G1L_pg_wal.tar --start 0/017A2610 --end 0/020934F8 2>&1 | tail -3 rmgr: Hash len (rec/tot): 72/ 72, tx: 720, lsn: 0/01FFC1B8, prev 0/01FFC178, desc: INSERT off 40, blkref #0: rel 1663/5/16397 blk 2, blkref #1: rel 1663/5/16397 blk 0 rmgr: Transaction len (rec/tot): 46/ 46, tx: 720, lsn: 0/01FFC200, prev 0/01FFC1B8, desc: COMMIT 2026-03-29 10:15:24.112967 NZDT pg_waldump: error: could not find WAL "000000010000000000000002" in archive "_tmp_H_1gv81G1L_pg_wal.tar" $ tar tvf _tmp_H_1gv81G1L_pg_wal.tar drwx------ 0 tmunro tmunro 0 Mar 29 10:15 archive_status/ -rw------- 0 tmunro tmunro 0 Mar 29 10:15 archive_status/000000010000000000000002.ready -rw------- 0 tmunro tmunro 0 Mar 29 10:15 archive_status/000000010000000000000001.ready drwx------ 0 tmunro tmunro 0 Mar 29 10:08 summaries/ -rw------- 0 tmunro tmunro 16777216 Mar 29 10:15 000000010000000000000002 -rw------- 0 tmunro tmunro 16777216 Mar 29 10:15 000000010000000000000001 -rw------- 0 tmunro tmunro 16777216 Mar 29 10:15 000000010000000000000003 It seems like the place we'd be looking for the file is in astreamer_tar_header(), so I added in some caveman debugging: /* * Parse key fields out of the header. */ fprintf(stderr, "XXXX [%s] XXXX\n", &buffer[TAR_OFFSET_NAME]); strlcpy(member->pathname, &buffer[TAR_OFFSET_NAME], MAXPGPATH); if (member->pathname[0] == '\0') pg_fatal("tar member has empty name"); Now I see: XXXX [archive_status/] XXXX XXXX [archive_status/000000010000000000000002.ready] XXXX XXXX [archive_status/000000010000000000000001.ready] XXXX XXXX [summaries/] XXXX XXXX [PaxHeader/000000010000000000000002] XXXX XXXX [GNUSparseFile.0/000000010000000000000002] XXXX XXXX [000000010000000000000001] XXXX rmgr: XLOG len (rec/tot): 30/ 30, tx: 0, lsn: 0/017A2610, prev 0/017A25F0, desc: NEXTOID 24576 rmgr: Standby len (rec/tot): 42/ 42, tx: 692, lsn: 0/017A2630, prev 0/017A2610, desc: LOCK xid 692 db 5 rel 16384 rmgr: Storage len (rec/tot): 42/ 42, tx: 692, lsn: 0/017A2660, prev 0/017A2630, desc: CREATE base/5/16384 ... lots more normal output ... rmgr: Hash len (rec/tot): 72/ 72, tx: 720, lsn: 0/01FFBED8, prev 0/01FFBE98, desc: INSERT off 97, blkref #0: rel 1663/5/16397 blk 2, blkref #1: rel 1663/5/16397 blk 0 rmgr: Heap len (rec/tot): 575/ 575, tx: 720, lsn: 0/01FFBF20, prev 0/01FFBED8, desc: INSERT off: 12, flags: 0x08, blkref #0: rel 1663/5/16393 blk 52 rmgr: Btree len (rec/tot): 64/ 64, tx: 720, lsn:XXXX [PaxHeader/000000010000000000000003] XXXX XXXX [GNUSparseFile.0/000000010000000000000003] XXXX 0/01FFC178, prev 0/01FFBF20, desc: INSERT_LEAF off: 344, blkref #0: rel 1663/5/16396 blk 2 rmgr: Hash len (rec/tot): 72/ 72, tx: 720, lsn: 0/01FFC1B8, prev 0/01FFC178, desc: INSERT off 40, blkref #0: rel 1663/5/16397 blk 2, blkref #1: rel 1663/5/16397 blk 0 rmgr: Transaction len (rec/tot): 46/ 46, tx: 720, lsn: 0/01FFC200, prev 0/01FFC1B8, desc: COMMIT 2026-03-29 10:15:24.112967 NZDT pg_waldump: error: could not find WAL "000000010000000000000002" in archive "_tmp_H_1gv81G1L_pg_wal.tar" Seems like it already stepped over 000000010000000000000002 earlier? Could it be a table-of-contents order dependency bug or something like that? -
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-28T21:49:29Z
Thomas Munro <thomas.munro@gmail.com> writes: > This fails 100% of the time on my machine, even after e9d72348 and ff84efe4, eg: > # Running: pg_waldump --path /tmp/D8WG1Sv2HE/pg_wal.tar --start > 0/017A2610 --end 0/02093848 > [09:43:29.288](0.148s) not ok 104 - runs with path option and start > and end locations: exit code 0 > [09:43:29.289](0.001s) # Failed test 'runs with path option and > start and end locations: exit code 0' > # at /home/tmunro/projects/postgresql/src/bin/pg_waldump/t/001_basic.pl > line 402. > [09:43:29.290](0.001s) not ok 105 - runs with path option and start > and end locations: no stderr > [09:43:29.291](0.001s) # Failed test 'runs with path option and > start and end locations: no stderr' > # at /home/tmunro/projects/postgresql/src/bin/pg_waldump/t/001_basic.pl > line 402. > [09:43:29.291](0.000s) # got: 'pg_waldump: error: could not > find WAL "000000010000000000000002" in archive "pg_wal.tar" > # ' Huh. What is your platform exactly? Maybe more to the point, what is the tar program you're using? > Seems like it already stepped over 000000010000000000000002 earlier? > Could it be a table-of-contents order dependency bug or something like > that? If you look at the TAP script, you'll see that it tries to randomize the order of the entries in the tar file (see sub generate_archive). So if that's the problem, it shouldn't reproduce 100%, and also we should be seeing lots of freckles on the buildfarm. We're not, so there must be something off-the-beaten-track about your test environment. I'm certainly more than prepared to believe there's still bugs lurking here, but we need to figure out why you're seeing it. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-03-28T22:08:43Z
On Sun, Mar 29, 2026 at 10:49 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: > Huh. What is your platform exactly? Maybe more to the point, > what is the tar program you're using? FreeBSD tar 3.8.2. > > Seems like it already stepped over 000000010000000000000002 earlier? > > Could it be a table-of-contents order dependency bug or something like > > that? > > If you look at the TAP script, you'll see that it tries to randomize > the order of the entries in the tar file (see sub generate_archive). > So if that's the problem, it shouldn't reproduce 100%, and also we > should be seeing lots of freckles on the buildfarm. We're not, so > there must be something off-the-beaten-track about your test > environment. Right, I see now. There is something different about 000000010000000000000002 though: it doesn't seem to have a normal (non-PAX, non-GNU) TOC entry, unlike000000010000000000000001. Trying to figure out why...
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-28T22:18:03Z
Thomas Munro <thomas.munro@gmail.com> writes: > It seems like the place we'd be looking for the file is in > astreamer_tar_header(), so I added in some caveman debugging: > XXXX [PaxHeader/000000010000000000000002] XXXX > XXXX [GNUSparseFile.0/000000010000000000000002] XXXX After a bit of research, I see the problem: your tar has decided to store 000000010000000000000002 as a sparse file, and evidently it inserts "GNUSparseFile.0/" into the file name when it does that. Our tar-parsing logic will not recognize that name as being one of the WAL files it wants; and even if it did, it doesn't know how to de-sparse the file contents, so it would get the wrong output. I'm pretty disinclined to try to support sparse files here, especially with ~10 days till feature freeze. However ... I do not find any indication in the GNU tar docs that it produces sparse files by default. It looks like you need to say -S/--sparse to make that happen. Maybe you have a version that's been hacked to make that the default? regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-28T22:36:57Z
I wrote: > However ... I do not find any indication in the GNU tar docs > that it produces sparse files by default. It looks like you > need to say -S/--sparse to make that happen. Maybe you have > a version that's been hacked to make that the default? Bleah. Digging in the man pages at freebsd.org, I read --read-sparse (c, r, u modes only) Read sparse file information from disk. This is the reverse of --no-read-sparse and the default behav- ior. It's apparently been there and been default since FreeBSD 13.1. This leads one to wonder how come BF member dikkop is managing to run this test successfully. I speculate that it's using a filesystem type that doesn't do sparse files (cc'ing Vondra for confirmation on that). It looks like to make this test stable on modern FreeBSD, we need to see if tar accepts --no-read-sparse and use that switch if so. regards, tom lane -
Re: pg_waldump: support decoding of WAL inside tarfile
Tomas Vondra <tomas@vondra.me> — 2026-03-28T22:57:37Z
On 3/28/26 23:36, Tom Lane wrote: > I wrote: >> However ... I do not find any indication in the GNU tar docs >> that it produces sparse files by default. It looks like you >> need to say -S/--sparse to make that happen. Maybe you have >> a version that's been hacked to make that the default? > > Bleah. Digging in the man pages at freebsd.org, I read > > --read-sparse > (c, r, u modes only) Read sparse file information from disk. > This is the reverse of --no-read-sparse and the default behav- > ior. > > It's apparently been there and been default since FreeBSD 13.1. > This leads one to wonder how come BF member dikkop is managing > to run this test successfully. I speculate that it's using a > filesystem type that doesn't do sparse files (cc'ing Vondra > for confirmation on that). > It's running on ufs. But I think the explanation is very simple. We had a short power outage on Thursday, and the FreeBSD machine failed to boot properly after the power was restored. IIUC this test is new, right? I fixed the machine, it'll start running the tests in a couple minutes. regards -- Tomas Vondra
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-28T23:12:11Z
Tomas Vondra <tomas@vondra.me> writes: > On 3/28/26 23:36, Tom Lane wrote: >> It's apparently been there and been default since FreeBSD 13.1. >> This leads one to wonder how come BF member dikkop is managing >> to run this test successfully. I speculate that it's using a >> filesystem type that doesn't do sparse files (cc'ing Vondra >> for confirmation on that). > It's running on ufs. But I think the explanation is very simple. We had > a short power outage on Thursday, and the FreeBSD machine failed to boot > properly after the power was restored. IIUC this test is new, right? Not that new, it dates to b15c15139, about a week ago. I've reproduced Thomas' failure on a local FreeBSD 15.0 image using zfs, and confirmed that this cowboy hack fixes it: diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl index 8bb8fa225f6..2904f4ef118 100644 --- a/src/bin/pg_waldump/t/001_basic.pl +++ b/src/bin/pg_waldump/t/001_basic.pl @@ -346,7 +346,7 @@ sub generate_archive # move into the WAL directory before archiving files my $cwd = getcwd; chdir($directory) || die "chdir: $!"; - command_ok([$tar, $compression_flags, $archive, @files]); + command_ok([$tar, $compression_flags, $archive, '--no-read-sparse', @files]); chdir($cwd) || die "chdir: $!"; } Of course we can't commit that, but maybe somebody wants to product-ize it. (Not me, at least not today.) regards, tom lane -
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-03-28T23:15:39Z
On Sun, Mar 29, 2026 at 11:37 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: > I wrote: > > However ... I do not find any indication in the GNU tar docs > > that it produces sparse files by default. It looks like you > > need to say -S/--sparse to make that happen. Maybe you have > > a version that's been hacked to make that the default? > > Bleah. Digging in the man pages at freebsd.org, I read > > --read-sparse > (c, r, u modes only) Read sparse file information from disk. > This is the reverse of --no-read-sparse and the default behav- > ior. > > It's apparently been there and been default since FreeBSD 13.1. > This leads one to wonder how come BF member dikkop is managing > to run this test successfully. I speculate that it's using a > filesystem type that doesn't do sparse files (cc'ing Vondra > for confirmation on that). > > It looks like to make this test stable on modern FreeBSD, > we need to see if tar accepts --no-read-sparse and use that > switch if so. Yeah. Here's my attempt at perl. I think your Mac probably has a similar tar program BTW... but apfs probably doesn't go around making holes visible to lseek() automatically or at least as eagerly as my ZFS system.
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-28T23:34:35Z
Thomas Munro <thomas.munro@gmail.com> writes: > On Sun, Mar 29, 2026 at 11:37 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: >> It looks like to make this test stable on modern FreeBSD, >> we need to see if tar accepts --no-read-sparse and use that >> switch if so. > Yeah. Here's my attempt at perl. Andrew might have some stylistic suggestions, but this looks plausible to me. > I think your Mac probably has a similar tar program BTW... but apfs > probably doesn't go around making holes visible to lseek() > automatically or at least as eagerly as my ZFS system. Interesting ... on my Tahoe laptop, I see $ tar --version bsdtar 3.5.3 - libarchive 3.7.4 zlib/1.2.12 liblzma/5.4.3 bz2lib/1.0.8 There is nothing in "tar --help" about sparse files, but experimentation shows that it accepts --[no-]read-sparse. Can't easily tell if the switch actually does anything. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-03-29T01:59:21Z
> On Mar 28, 2026, at 7:34 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote: > > Thomas Munro <thomas.munro@gmail.com> writes: >>> On Sun, Mar 29, 2026 at 11:37 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: >>> It looks like to make this test stable on modern FreeBSD, >>> we need to see if tar accepts --no-read-sparse and use that >>> switch if so. > >> Yeah. Here's my attempt at perl. > > Andrew might have some stylistic suggestions, but this looks > plausible to me. Looks basically ok to me, although I wouldn’t make the variable name all caps. Cheers Andrew
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-03-29T02:01:24Z
On Sun, Mar 29, 2026 at 2:59 PM Andrew Dunstan <andrew@dunslane.net> wrote: > Looks basically ok to me, although I wouldn’t make the variable name all caps. Will fix and push. Thanks!
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tomas Vondra <tomas@vondra.me> — 2026-03-29T13:33:10Z
On 3/29/26 00:12, Tom Lane wrote: > Tomas Vondra <tomas@vondra.me> writes: >> On 3/28/26 23:36, Tom Lane wrote: >>> It's apparently been there and been default since FreeBSD 13.1. >>> This leads one to wonder how come BF member dikkop is managing >>> to run this test successfully. I speculate that it's using a >>> filesystem type that doesn't do sparse files (cc'ing Vondra >>> for confirmation on that). > >> It's running on ufs. But I think the explanation is very simple. We had >> a short power outage on Thursday, and the FreeBSD machine failed to boot >> properly after the power was restored. IIUC this test is new, right? > > Not that new, it dates to b15c15139, about a week ago. > > I've reproduced Thomas' failure on a local FreeBSD 15.0 image > using zfs, and confirmed that this cowboy hack fixes it: > Interesting. Then I guess it has to be due to some difference in ufs vs. zfs, when handling sparse files. It might be useful to add a bit more variation here, and switch some of the animals to non-default filesystems (not just the FreeBSD ones, which we seem to have only two that run reasonably often). I'd bet most of the linux systems run on ext4/xfs, few on btrfs/zfs. regards -- Tomas Vondra
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-03-29T22:11:50Z
On Mon, Mar 30, 2026 at 2:33 AM Tomas Vondra <tomas@vondra.me> wrote: > On 3/29/26 00:12, Tom Lane wrote: > > I've reproduced Thomas' failure on a local FreeBSD 15.0 image > > using zfs, and confirmed that this cowboy hack fixes it: > > > > Interesting. Then I guess it has to be due to some difference in ufs vs. > zfs, when handling sparse files. It might be useful to add a bit more > variation here, and switch some of the animals to non-default > filesystems (not just the FreeBSD ones, which we seem to have only two > that run reasonably often). I'd bet most of the linux systems run on > ext4/xfs, few on btrfs/zfs. UFS does have sparse files (its ancestor invented them some time around (time_t) 0), it just doesn't make them unless you tell it to. PostgreSQL only does that if you set wal_init_zero=false. ZFS is different because it creates holes automagically when you write zeroes, at least if compression is enabled so it has to scan all your bytes anyway. I was curious to know if BTRFS does that too, or hides zero-compression at some lower invisible level: $ echo "hello" > 1MB-sparse.dat $ truncate -s 512KB 1MB-sparse.dat $ echo "world" >> 1MB-sparse.dat $ truncate -s 1MB 1MB-sparse.dat $ ls -l 1MB-sparse.dat -rw-rw-r-- 1 tmunro tmunro 1000000 Mar 30 10:11 1MB-sparse.dat $ du -hs 1MB-sparse.dat 8.0K 1MB-sparse.dat $ strace tar -S -cf foo.tar 1MB-sparse.dat 2>&1 | grep seek lseek(4, 0, SEEK_DATA) = 0 lseek(4, 0, SEEK_HOLE) = 4096 lseek(4, 4096, SEEK_DATA) = 512000 lseek(4, 512000, SEEK_HOLE) = 516096 lseek(4, 516096, SEEK_DATA) = -1 ENXIO (No such device or address) ... so that's a yes, lseek sees holes that we didn't ask it to make, just like on ZFS, but the rest of this trace of GNU tar -S -cf is interesting: lseek(5, 0, SEEK_SET) = 0 lseek(5, 0, SEEK_SET) = 0 lseek(4, 0, SEEK_SET) = 0 lseek(4, 512000, SEEK_SET) = 512000 lseek(4, 1000000, SEEK_SET) = 1000000 It didn't write out PAX format! Instead it replicated the holes into the tar file itself with SEEK_SET. $ strings foo.tar | grep Sparse You have to add --format=posix to enable the GNU behaviour that BSD tar is emulating by default: $ tar --format=posix -S -cf foo.tar 1MB-sparse.dat $ strings foo.tar | grep Sparse ./GNUSparseFile.4190/1MB-sparse.dat I expected GNU tar to be forced to do that if writing to non-seekable output, eg "tar -S -c 1MB-sparse.dat | cat > foo.tar", but somehow it manages to write out only ~10KB of plain ustar format that it is able to restore to the full 1MB apparent size using some other trick, but ... ENOTIME, I dunno how it's doing that. Might be interesting to see if pg_waldump can read it though, 'cause the bytes aren't all there. BTW I confirmed that Apple tar does have -S by default too, it's just that APFS doesn't make holes magically, so this test would presumably have broken on a Mac if wal_init_zero had been forced to zero (not tested). Anyway, given the defaults, GNU tar + ZFS/BTRFS users must be pretty unlikely to hit this in the wild, and the symptom is a confusing error in a maintenance tool, not corruption, so I don't think this is a big deal. I might still try teaching the astreamer code to understand PAX 1.0 when it sees it in the next cycle though, for the benefit of FreeBSD users. A quick and dirty version could probably just unmangle the name and skip the first block of data, since any valid WAL file will not begin with a hole and valid WAL data will end at the first hole and fail our verification, but of course a real implementation should read the map properly[1]... [1] https://www.gnu.org/software/tar/manual/html_node/PAX-1.html
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-03-29T22:20:24Z
On Mon, Mar 30, 2026 at 11:11 AM Thomas Munro <thomas.munro@gmail.com> wrote: > ... so that's a yes, lseek sees holes that we didn't ask it to make, Oops, sorry, I wrote that email too fast and got my examples mixed up, BTFS actually *doesnt* do that automatically, that was of course a trace showing a file with explicitly made holes. So this is probably be a ZFS-only issue unless you're using wal_init_zero=0, and then any file system could result in PAX-sparse-format tarballs, but even then only if you use non-default switches that in practice no one will use with GNU tar, or if you use BSD tar. So in practice this is a FreeBSD-only issue.
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-03-29T22:23:31Z
Thomas Munro <thomas.munro@gmail.com> writes: > Anyway, given the defaults, GNU tar + ZFS/BTRFS users must be pretty > unlikely to hit this in the wild, and the symptom is a confusing error > in a maintenance tool, not corruption, so I don't think this is a big > deal. I might still try teaching the astreamer code to understand PAX > 1.0 when it sees it in the next cycle though, for the benefit of > FreeBSD users. I agree that this isn't too critical if the effects are confined to pg_waldump. I believe that pg_basebackup and pg_verifybackup also use astreamer_tar.c, but it's not clear to me if they'd ever be asked to parse files made by tar(1) and not by our own sparseness-ignorant tar-writing code. If they can be, that'd be a higher-priority reason to fill in this gap. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-04-01T02:05:53Z
On Mon, Mar 30, 2026 at 11:23 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: > Thomas Munro <thomas.munro@gmail.com> writes: > > Anyway, given the defaults, GNU tar + ZFS/BTRFS users must be pretty > > unlikely to hit this in the wild, and the symptom is a confusing error > > in a maintenance tool, not corruption, so I don't think this is a big > > deal. I might still try teaching the astreamer code to understand PAX > > 1.0 when it sees it in the next cycle though, for the benefit of > > FreeBSD users. > > I agree that this isn't too critical if the effects are confined to > pg_waldump. I believe that pg_basebackup and pg_verifybackup also use > astreamer_tar.c, but it's not clear to me if they'd ever be asked to > parse files made by tar(1) and not by our own sparseness-ignorant > tar-writing code. If they can be, that'd be a higher-priority reason > to fill in this gap. I pushed the workaround for the test. Yeah I can't see any reason why pg_verifybackup --wal-path=foo.tar won't suffer the same problem in the wild. Again, it's not the end of the world because it'll just fail and you'll probably eventually figure out why. So perhaps we should just improve our detection of archives that we can't handle? Straw man algorithm: If you can't find $NAME in the archive, then check if PaxHeaders/$NAME exists, and if so, fail with 'unsupported TAR format for WAL file "%s" in archive "%s"' instead. That'd probably work well enough in practice, because astreamer_tar.c treats PAX extended header pseudo-files as regular files (they're not, they have type 'x'), and both GNU and BSD tar happen to use that. POSIX doesn't require that naming, so it would in theory be more correct to teach astreamer_tar.c to recognise PAX extended headers and fish out enough information and link it to the following archive member, but a simple test to improve error messaging seems like the right level of effort here. Here's a test patch that shows the problem on any system with GNU tar or BSD tar and a file system that supports sparse files. The test succeeds because it looks for "error: could not find WAL" but the idea would be to change it to look for a new error message like that. My motivation was to make this reproducible on any system, in case that's helpful for Amul and Andrew if they're interested in trying to improve this edge case in time for the release. Otherwise I'll come back to it, but probably not in time...
-
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-04-01T10:39:05Z
On 2026-03-31 Tu 10:05 PM, Thomas Munro wrote: > On Mon, Mar 30, 2026 at 11:23 AM Tom Lane<tgl@sss.pgh.pa.us> wrote: >> Thomas Munro<thomas.munro@gmail.com> writes: >>> Anyway, given the defaults, GNU tar + ZFS/BTRFS users must be pretty >>> unlikely to hit this in the wild, and the symptom is a confusing error >>> in a maintenance tool, not corruption, so I don't think this is a big >>> deal. I might still try teaching the astreamer code to understand PAX >>> 1.0 when it sees it in the next cycle though, for the benefit of >>> FreeBSD users. >> I agree that this isn't too critical if the effects are confined to >> pg_waldump. I believe that pg_basebackup and pg_verifybackup also use >> astreamer_tar.c, but it's not clear to me if they'd ever be asked to >> parse files made by tar(1) and not by our own sparseness-ignorant >> tar-writing code. If they can be, that'd be a higher-priority reason >> to fill in this gap. > I pushed the workaround for the test. It occurred to me this morning that we probably shouldn't run this test on Windows, and if we do we shouldn't be using /dev/null (the Windows equivalent of which is just "nul"). The simplest fix would just be to add a "!$windows_os" to the if test. cheers andrew -- Andrew Dunstan EDB:https://www.enterprisedb.com
-
Re: pg_waldump: support decoding of WAL inside tarfile
Andres Freund <andres@anarazel.de> — 2026-04-01T13:26:00Z
Hi, On 2026-04-01 06:39:05 -0400, Andrew Dunstan wrote: > > On 2026-03-31 Tu 10:05 PM, Thomas Munro wrote: > > On Mon, Mar 30, 2026 at 11:23 AM Tom Lane<tgl@sss.pgh.pa.us> wrote: > > > Thomas Munro<thomas.munro@gmail.com> writes: > > > > Anyway, given the defaults, GNU tar + ZFS/BTRFS users must be pretty > > > > unlikely to hit this in the wild, and the symptom is a confusing error > > > > in a maintenance tool, not corruption, so I don't think this is a big > > > > deal. I might still try teaching the astreamer code to understand PAX > > > > 1.0 when it sees it in the next cycle though, for the benefit of > > > > FreeBSD users. > > > I agree that this isn't too critical if the effects are confined to > > > pg_waldump. I believe that pg_basebackup and pg_verifybackup also use > > > astreamer_tar.c, but it's not clear to me if they'd ever be asked to > > > parse files made by tar(1) and not by our own sparseness-ignorant > > > tar-writing code. If they can be, that'd be a higher-priority reason > > > to fill in this gap. > > I pushed the workaround for the test. > > > It occurred to me this morning that we probably shouldn't run this test on > Windows, and if we do we shouldn't be using /dev/null (the Windows > equivalent of which is just "nul"). The simplest fix would just be to add a > "!$windows_os" to the if test. Why should we skip this test on windows? I think we have historically been way too liberal about sprinkling !$windows_os test disablements around. More than once there were actual bugs that we just swept under the rug by disabling the tests that detected them. Either we support windows or we don't. Greetings, Andres Freund
-
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-04-01T14:19:32Z
On 2026-04-01 We 9:26 AM, Andres Freund wrote: > Hi, > > On 2026-04-01 06:39:05 -0400, Andrew Dunstan wrote: >> On 2026-03-31 Tu 10:05 PM, Thomas Munro wrote: >>> On Mon, Mar 30, 2026 at 11:23 AM Tom Lane<tgl@sss.pgh.pa.us> wrote: >>>> Thomas Munro<thomas.munro@gmail.com> writes: >>>>> Anyway, given the defaults, GNU tar + ZFS/BTRFS users must be pretty >>>>> unlikely to hit this in the wild, and the symptom is a confusing error >>>>> in a maintenance tool, not corruption, so I don't think this is a big >>>>> deal. I might still try teaching the astreamer code to understand PAX >>>>> 1.0 when it sees it in the next cycle though, for the benefit of >>>>> FreeBSD users. >>>> I agree that this isn't too critical if the effects are confined to >>>> pg_waldump. I believe that pg_basebackup and pg_verifybackup also use >>>> astreamer_tar.c, but it's not clear to me if they'd ever be asked to >>>> parse files made by tar(1) and not by our own sparseness-ignorant >>>> tar-writing code. If they can be, that'd be a higher-priority reason >>>> to fill in this gap. >>> I pushed the workaround for the test. >> >> It occurred to me this morning that we probably shouldn't run this test on >> Windows, and if we do we shouldn't be using /dev/null (the Windows >> equivalent of which is just "nul"). The simplest fix would just be to add a >> "!$windows_os" to the if test. > Why should we skip this test on windows? > > I think we have historically been way too liberal about sprinkling > !$windows_os test disablements around. More than once there were actual bugs > that we just swept under the rug by disabling the tests that detected them. > Either we support windows or we don't. > Maybe I misunderstood, but I didn't think this was going to be an issue on NTFS. In general I agree with you, though. I try to avoid skipping things on Windows. cheers andrew -- Andrew Dunstan EDB: https://www.enterprisedb.com
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-04-01T18:25:40Z
Thomas Munro <thomas.munro@gmail.com> writes: > On Mon, Mar 30, 2026 at 11:23 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: >> I agree that this isn't too critical if the effects are confined to >> pg_waldump. I believe that pg_basebackup and pg_verifybackup also use >> astreamer_tar.c, but it's not clear to me if they'd ever be asked to >> parse files made by tar(1) and not by our own sparseness-ignorant >> tar-writing code. If they can be, that'd be a higher-priority reason >> to fill in this gap. > Yeah I can't see any reason why pg_verifybackup --wal-path=foo.tar > won't suffer the same problem in the wild. Again, it's not the end of > the world because it'll just fail and you'll probably eventually > figure out why. So perhaps we should just improve our detection of > archives that we can't handle? After reading the POSIX spec for pax format (in the pax(1) man page), I think it's absolutely essential that we reject files that contain pax extension headers. Those can change the interpretation of the following file header(s) in nearly arbitrary ways, so we have plenty of problems besides this sparse-file issue if we just ignore them. (Of course, later we can consider improving the code to handle them correctly, but that ain't happening in time for v19.) Also, if we are admitting the possibility that what we are reading was made by a platform-supplied tar and not our own code, I think it verges on lunacy to behave as though unsupported typeflags are regular files. So I think we need something more or less like the attached. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-04-02T00:16:17Z
On Thu, Apr 2, 2026 at 7:25 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: > Also, if we are admitting the possibility that what we are reading > was made by a platform-supplied tar and not our own code, I think > it verges on lunacy to behave as though unsupported typeflags are > regular files. Yeah, if this is the first time we parse files we didn't make then that makes total sense. I was a bit unsure of that question when I suggested we reject pax only after we've failed to find a file, in case there are scenarios that work today with harmless ignorable pax headers that don't change the file name. > So I think we need something more or less like the attached. LGTM. Tested with both tars here. I updated that little test patch for this. Not sure if you think it's worth a test though, now that it's so simple. @Andrew: I tried using File::Spec->devnull() this time. Are you able to check if this works OK on Windows, applied on top of Tom's patch? AFAIK should be able to run this new test and pass, not skip it. But it could be that the shell invocation needs tweaking. It's hard to tell from CI. (Huh, apparently Windows ships a copy of BSD tar as C:\Windows\System32\tar.exe these days.)
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-04-02T01:22:44Z
Thomas Munro <thomas.munro@gmail.com> writes: > On Thu, Apr 2, 2026 at 7:25 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: >> Also, if we are admitting the possibility that what we are reading >> was made by a platform-supplied tar and not our own code, I think >> it verges on lunacy to behave as though unsupported typeflags are >> regular files. > Yeah, if this is the first time we parse files we didn't make then > that makes total sense. I was a bit unsure of that question when I > suggested we reject pax only after we've failed to find a file, in > case there are scenarios that work today with harmless ignorable pax > headers that don't change the file name. Of course this is all new so far as pg_waldump is concerned. I'm a bit unclear about whether pg_verifybackup's exposure is large enough to warrant back-patching any of this. Looking again at astreamer_tar.c, I suddenly realized that it doesn't do any meaningful input validation. So if you feed it junk input, you get garbage errors that aren't even predictable: $ head -c 32768 /dev/urandom >junk.tar $ pg_waldump --path junk.tar --start 1/0x10000000 pg_waldump: error: COPY stream ended before last file was finished $ head -c 32768 /dev/urandom >junk.tar $ pg_waldump --path junk.tar --start 1/0x10000000 pg_waldump: error: tar member has empty name $ head -c 32768 /dev/urandom >junk.tar $ pg_waldump --path junk.tar --start 1/0x10000000 pg_waldump: error: COPY stream ended before last file was finished $ head -c 32768 /dev/urandom >junk.tar $ pg_waldump --path junk.tar --start 1/0x10000000 pg_waldump: error: could not find WAL in archive "junk.tar" tar itself is considerably saner: $ tar tf junk.tar tar: This does not look like a tar archive tar: Skipping to next header tar: Exiting with failure status due to previous errors So I think we need something like the attached, in addition to what I sent before. This just makes astreamer_tar.c use the isValidTarHeader function that pg_dump already had. (I decided to const-ify isValidTarHeader's argument while moving it to a shared location, which in turn requires const-ifying tarChecksum.) regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-04-02T02:14:09Z
On Thu, Apr 2, 2026 at 2:22 PM Tom Lane <tgl@sss.pgh.pa.us> wrote: > Looking again at astreamer_tar.c, I suddenly realized that it doesn't > do any meaningful input validation. So if you feed it junk input, > you get garbage errors that aren't even predictable: Wow. > So I think we need something like the attached, in addition > to what I sent before. This just makes astreamer_tar.c use > the isValidTarHeader function that pg_dump already had. > (I decided to const-ify isValidTarHeader's argument while > moving it to a shared location, which in turn requires > const-ifying tarChecksum.) LGTM. $ echo -n x | dd of=foo.tar bs=1 seek=257 count=1 conv=notrunc $ strings foo.tar | grep tar | head -1 xstar $ pg_waldump --path=foo.tar -s 0/1 -e 0/100 pg_waldump: error: input file does not appear to be a valid tar archive $ echo -n u | dd of=foo.tar bs=1 seek=257 count=1 conv=notrunc $ strings foo.tar | grep tar | head -1 ustar $ pg_waldump --path=foo.tar -s 0/1 -e 0/100 ... other output...
-
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-04-02T11:36:25Z
On 2026-04-01 We 8:16 PM, Thomas Munro wrote: > On Thu, Apr 2, 2026 at 7:25 AM Tom Lane<tgl@sss.pgh.pa.us> wrote: >> Also, if we are admitting the possibility that what we are reading >> was made by a platform-supplied tar and not our own code, I think >> it verges on lunacy to behave as though unsupported typeflags are >> regular files. > Yeah, if this is the first time we parse files we didn't make then > that makes total sense. I was a bit unsure of that question when I > suggested we reject pax only after we've failed to find a file, in > case there are scenarios that work today with harmless ignorable pax > headers that don't change the file name. > >> So I think we need something more or less like the attached. > LGTM. Tested with both tars here. I updated that little test patch > for this. Not sure if you think it's worth a test though, now that > it's so simple. > > @Andrew: I tried usingFile::Spec->devnull() this time. Are you able > to check if this works OK on Windows, applied on top of Tom's patch? > AFAIK should be able to run this new test and pass, not skip it. But > it could be that the shell invocation needs tweaking. It's hard to > tell from CI. (Huh, apparently Windows ships a copy of BSD tar as > C:\Windows\System32\tar.exe these days.) Yes, that appears to work. I would put a "2>&1" at the end - we don't care about the output, just whether or not it succeeds: C:\Windows\system32>perl -MFile::Spec -e "print File::Spec->devnull();" nul C:\Windows\system32>tar --no-read-sparse -c - nul > nul 2>&1 && echo hello C:\Windows\system32>tar --no-read-sparse -c - nul > nul 2>&1 || echo hello hello cheers andrew -- Andrew Dunstan EDB:https://www.enterprisedb.com
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-04-02T16:29:52Z
Thomas Munro <thomas.munro@gmail.com> writes: > On Thu, Apr 2, 2026 at 2:22 PM Tom Lane <tgl@sss.pgh.pa.us> wrote: >> So I think we need something like the attached, in addition >> to what I sent before. This just makes astreamer_tar.c use >> the isValidTarHeader function that pg_dump already had. > LGTM. Pushed, thanks for reviewing! In the event I decided to back-patch to v18, where these fixes could protect pg_verifybackup against tar files it can't handle. In older branches the astreamer (nee bbstreamer) logic only exists inside pg_basebackup, and IIUC that's not really exposed to tar files that weren't made by our own code. So I did not bother with back-patching further, though it'd be possible to do that if someone knows a scenario where it'd matter. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tomas Vondra <tomas@vondra.me> — 2026-04-02T17:13:08Z
On 4/2/26 18:29, Tom Lane wrote: > Thomas Munro <thomas.munro@gmail.com> writes: >> On Thu, Apr 2, 2026 at 2:22 PM Tom Lane <tgl@sss.pgh.pa.us> wrote: >>> So I think we need something like the attached, in addition >>> to what I sent before. This just makes astreamer_tar.c use >>> the isValidTarHeader function that pg_dump already had. > >> LGTM. > > Pushed, thanks for reviewing! In the event I decided to back-patch to > v18, where these fixes could protect pg_verifybackup against tar files > it can't handle. In older branches the astreamer (nee bbstreamer) > logic only exists inside pg_basebackup, and IIUC that's not really > exposed to tar files that weren't made by our own code. So I did > not bother with back-patching further, though it'd be possible to > do that if someone knows a scenario where it'd matter. > It seems jay/hippopotamus failed on this, for some reason. Those two animals are on the same host, just using different compilers. I did update+reboot the machine this afternoon (before the test), but I don't see why that would cause the failure. Maybe there's something special about OpenSUSE? -- Tomas Vondra
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-04-02T17:43:36Z
Tomas Vondra <tomas@vondra.me> writes: > On 4/2/26 18:29, Tom Lane wrote: >> Pushed, thanks for reviewing! In the event I decided to back-patch to >> v18, where these fixes could protect pg_verifybackup against tar files >> it can't handle. > It seems jay/hippopotamus failed on this, for some reason. Hmm: # Failed test 'corrupt backup fails verification: extra_file: matches' # at t/003_corruption.pl line 198. # 'pg_verifybackup: error: pax extensions to tar format are not supported # ' # doesn't match '(?^:extra_file.*present (on disk|in archive "[^"]+") but not in the manifest)' > Maybe there's something special about OpenSUSE? Apparently its version of "tar" will produce pax-extended files at the drop of a hat. I have an OpenSUSE image around here somewhere, will see if I can reproduce this. But while I'm asking, what filesystem are those animals running on top of? regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tomas Vondra <tomas@vondra.me> — 2026-04-02T18:08:18Z
On 4/2/26 19:43, Tom Lane wrote: > Tomas Vondra <tomas@vondra.me> writes: >> On 4/2/26 18:29, Tom Lane wrote: >>> Pushed, thanks for reviewing! In the event I decided to back-patch to >>> v18, where these fixes could protect pg_verifybackup against tar files >>> it can't handle. > >> It seems jay/hippopotamus failed on this, for some reason. > > Hmm: > > # Failed test 'corrupt backup fails verification: extra_file: matches' > # at t/003_corruption.pl line 198. > # 'pg_verifybackup: error: pax extensions to tar format are not supported > # ' > # doesn't match '(?^:extra_file.*present (on disk|in archive "[^"]+") but not in the manifest)' > >> Maybe there's something special about OpenSUSE? > > Apparently its version of "tar" will produce pax-extended files > at the drop of a hat. I have an OpenSUSE image around here > somewhere, will see if I can reproduce this. But while I'm > asking, what filesystem are those animals running on top of? > btrfs -- Tomas Vondra
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-04-02T18:47:52Z
Tomas Vondra <tomas@vondra.me> writes: > On 4/2/26 19:43, Tom Lane wrote: >> Tomas Vondra <tomas@vondra.me> writes: >>> Maybe there's something special about OpenSUSE? >> Apparently its version of "tar" will produce pax-extended files >> at the drop of a hat. I have an OpenSUSE image around here >> somewhere, will see if I can reproduce this. But while I'm >> asking, what filesystem are those animals running on top of? > btrfs Yup, so capable of making sparse WAL files. I can reproduce the problem here, and what I see is > tar --version tar (GNU tar) 1.34 Copyright (C) 2021 Free Software Foundation, Inc. ... > tar -? ... -H, --format=FORMAT create archive of the given format FORMAT is one of the following: gnu GNU tar 1.13.x format oldgnu GNU format as per tar <= 1.12 pax POSIX 1003.1-2001 (pax) format posix same as pax ustar POSIX 1003.1-1988 (ustar) format v7 old V7 tar format ... *This* tar defaults to: --format=posix -f- -b20 --quoting-style=escape --rmt-command=/usr/bin/rmt --rsh-command=/usr/bin/ssh So there you have it: pax format by default. This is unlike what I see on RHEL or Fedora: ... *This* tar defaults to: --format=gnu -f- -b20 --quoting-style=escape --rmt-command=/etc/rmt --rsh-command=/usr/bin/ssh So it looks like we need a switch hack similar to what we did for BSD tar, but injecting "--format=gnu" (or perhaps "--format=ustar"?) if the tar program will take that. Interestingly, pg_verifybackup's t/003_corruption.pl test also fails with the same issue, so apparently this platform is even more aggressive about sparse-ifying files than Thomas' FreeBSD box. I wonder how come we managed to pass that test case before on these machines. I'm inclined to push the logic for selecting these tar options into some common subroutine in Test::Utils, rather than having two copies (and maybe more later). regards, tom lane -
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-04-02T19:15:36Z
I wrote: > Interestingly, pg_verifybackup's t/003_corruption.pl test also fails > with the same issue, so apparently this platform is even more > aggressive about sparse-ifying files than Thomas' FreeBSD box. > I wonder how come we managed to pass that test case before on > these machines. The answer to that seems to be that the test scripts for pg_verifybackup simply fail to detect when it's mishandling sparse tar entries. We only check failing cases not successful cases, and in each case the error checked for is independent of whether we would have extracted WAL data correctly. Grumble. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-04-02T22:41:33Z
On Fri, Apr 3, 2026 at 7:48 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: > *This* tar defaults to: > --format=posix -f- -b20 --quoting-style=escape --rmt-command=/usr/bin/rmt > --rsh-command=/usr/bin/ssh From some quick googling, OpenSUSE is the only distro I could find that does this, but the fine manual says they plan to make pax the default upstream so eventually it might be everywhere: https://www.gnu.org/software/tar/manual/html_section/Formats.html > Interestingly, pg_verifybackup's t/003_corruption.pl test also fails > with the same issue, so apparently this platform is even more > aggressive about sparse-ifying files than Thomas' FreeBSD box. Looks like sparse files and BTRFS might be a red herring then, if it's simply been told to put a pax header on every file? I think my system might be defaulting to "restricted pax", meaning that pax headers are added to individual files only if necessary, path-too-long etc, magic sparse format triggered, etc. https://man.freebsd.org/cgi/man.cgi?query=libarchive-formats > I'm inclined to push the logic for selecting these tar options > into some common subroutine in Test::Utils, rather than having > two copies (and maybe more later). How about using --format=ustar, instead of that sparse control stuff? That solves the BSD tar + ZFS problem and the OpenSUSE GNU tar default format problem in one fell swoop, and actually says what we want explicitly. Should we have a <note> near the --path/--wal-path documentation to state explicitly that usta format is required, and that pax is not [yet] supported, and that --format=ustar might be required on some systems?
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-04-02T22:50:50Z
Thomas Munro <thomas.munro@gmail.com> writes: > On Fri, Apr 3, 2026 at 7:48 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: >> Interestingly, pg_verifybackup's t/003_corruption.pl test also fails >> with the same issue, so apparently this platform is even more >> aggressive about sparse-ifying files than Thomas' FreeBSD box. > Looks like sparse files and BTRFS might be a red herring then, if it's > simply been told to put a pax header on every file? It looked to me that it was only putting a pax header on sparse-ified WAL files. > How about using --format=ustar, instead of that sparse control stuff? I did it that way for GNU tar, but did not research whether bsdtar will take that option. Feel free to hack on ebba64c08 some more. (It seems though that the two tars' locutions for "write to stdout" are different, so we might have to have separate tests even if they end up pushing the same option.) regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Sami Imseih <samimseih@gmail.com> — 2026-04-02T23:43:33Z
> >> So I think we need something like the attached, in addition > >> to what I sent before. This just makes astreamer_tar.c use > >> the isValidTarHeader function that pg_dump already had. > > > LGTM. > > Pushed, thanks for reviewing! In the event I decided to back-patch to > v18, where these fixes could protect pg_verifybackup against tar files > it can't handle. Hi, I just encountered a regression test failure for pg_waldump due to ebba64c08d9. ```` ――――――――――――――――――――――――――――――――――――――――――――――――――――― ✀ ―――――――――――――――――――――――――――――――――――――――――――――――――――――― Listing only the last 100 lines from a long log. # at /local/home/simseih/pgdev/installations/worktrees/dev/src/bin/pg_waldump/t/001_basic.pl line 432. # got: 'pg_waldump: error: could not find WAL in archive "pg_wal.tar.gz" # ' # expected: '' ``` and regress_log_001_basic shows this: ``` # Running: /usr/bin/tar --format=ustar -cf /tmp/ja26rXZOnb/pg_wal.tar archive_status 000000010000000000000002 000000010000000000000001 summaries 000000010000000000000003 [22:25:00.525](0.008s) not ok 101 [22:25:00.525](0.000s) # Failed test at /local/home/simseih/pgdev/installations/worktrees/dev/src/bin/pg_waldump/t/001_basic.pl line 350. [22:25:00.525](0.000s) # ---------- command failed ---------- [22:25:00.526](0.000s) # /usr/bin/tar --format=ustar -cf /tmp/ja26rXZOnb/pg_wal.tar archive_status 000000010000000000000002 000000010000000000000001 summaries 000000010000000000000003 [22:25:00.526](0.000s) # -------------- stderr -------------- [22:25:00.526](0.000s) # /usr/bin/tar: value 10012663 out of uid_t range 0..2097151 ``` The --format=ustar has a limit of 2^21 (2097151) for UID/GID [1] and on my machine the UID is 10012663. So I found that one way to deal with this is to run the tar command with --owner=0 --group=0. As far as I can tell, the owner and group IDs don't matter for these tests, so maybe that is OK. @@ -1333,6 +1333,10 @@ sub tar_portability_options == 0) { push(@tar_p_flags, "--format=ustar"); + # ustar format supports UIDs only up to 2^21 (2097151). + # Override owner/group to avoid failures on systems where + # the running user's UID/GID exceeds that limit. + push(@tar_p_flags, "--owner=0", "--group=0"); } While this fixes the test, I am now not sure what the broader implications are for --format=ustar for pg_waldump in the broader discussion? [1] [https://www.gnu.org/software/tar/manual/html_section/Formats.html] -- Sami Imseih Amazon Web Services (AWS) -
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-04-02T23:49:21Z
On Fri, Apr 3, 2026 at 11:50 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: > > How about using --format=ustar, instead of that sparse control stuff? > > I did it that way for GNU tar, but did not research whether bsdtar > will take that option. Feel free to hack on ebba64c08 some more. > > (It seems though that the two tars' locutions for "write to stdout" > are different, so we might have to have separate tests even if they > end up pushing the same option.) I have: $ tar --version bsdtar 3.8.2 - libarchive 3.8.2 zlib/1.3.1 liblzma/5.8.1 libzstd/1.5.2 openssl/3.5.4 libb2/bundled $ gtar --version tar (GNU tar) 1.35 Copyright (C) 2023 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Written by John Gilmore and Jay Fenlason. This seems to work for both: $ tar --format=ustar -c /dev/null > /dev/null tar: Removing leading '/' from member names $ gtar --format=ustar -c /dev/null > /dev/null gtar: Removing leading `/' from member names The attached passes with both, and regress_log_001_basic looks like: # Running: /usr/bin/tar --format=ustar -cf /tmp/J_ifbfUOSd/pg_wal.tar archive_status 000000010000000000000001 000000010000000000000003 000000010000000000000002 summaries [12:12:24.301](0.072s) ok 101 # Running: /usr/local/bin/gtar --format=ustar -cf /tmp/pbdsHdrAdw/pg_wal.tar 000000010000000000000002 archive_status 000000010000000000000003 summaries 000000010000000000000001 [12:18:14.739](0.050s) ok 101 I think a Windows system could be using either. BSD tar comes pre-installed by Microsoft and people often install GNU tools. So I think we should use File::Spec->devnull() instead of /dev/null, and Andrew showed that working. I doubt Windows is capable of making sparse files (except perhaps with ReFS?), but it's nice to use the same code everywhere and future-proof in case GNU carries out its thread to switch to pax by default. Windows probably has file attributes that ustar can't represent (?), so I guess that might motivate it to use pax headers if they are indeed added only when needed. Longer term I think we need to tolerate but ignore pax headers. If I understand the spirit of this long evolution, pax archives are intended to be acceptable to pre-pax implementations, which implies that they can't really change the meaning of the bits of the file contents. That's why GNU's --sparse hides funky file encodings from old tars by renaming them to GNUSparseFile.%p/%f, and that leads back to my original suggestion that we should figure out how to detect and reject pax only if we failed to find the file under the expected name. (Or of course we could just implement support for that, and I have a half-baked trial patch for that but now is not the time.)
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-04-03T00:07:40Z
On Fri, Apr 3, 2026 at 12:43 PM Sami Imseih <samimseih@gmail.com> wrote: > The --format=ustar has a limit of 2^21 (2097151) for UID/GID [1] > and on my machine the UID is 10012663. > > So I found that one way to deal with this is to run the tar command with > --owner=0 --group=0. As far as I can tell, the owner and group IDs don't > matter for these tests, so maybe that is OK. > > @@ -1333,6 +1333,10 @@ sub tar_portability_options > == 0) > { > push(@tar_p_flags, "--format=ustar"); > + # ustar format supports UIDs only up to 2^21 (2097151). > + # Override owner/group to avoid failures on systems where > + # the running user's UID/GID exceeds that limit. > + push(@tar_p_flags, "--owner=0", "--group=0"); Interesting. BSD tar accepts those too, so here's an update to my previous patch. > While this fixes the test, I am now not sure what the broader implications are > for --format=ustar for pg_waldump in the broader discussion? I think users who have their own tar scripts will mostly be unaffected, but a small minority will see the new error if they try to use pg_verifybackup or pg_waldump, and they'll find their way to --format=ustar, and then they might see the UID/GID error in their own .tar production scripts, and find their way to adding those switches too. Seems OK? Especially with a documentation note once we've settle all of this. -
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-04-03T00:11:37Z
Thomas Munro <thomas.munro@gmail.com> writes: > On Fri, Apr 3, 2026 at 11:50 AM Tom Lane <tgl@sss.pgh.pa.us> wrote: >>> How about using --format=ustar, instead of that sparse control stuff? >> I did it that way for GNU tar, but did not research whether bsdtar >> will take that option. Feel free to hack on ebba64c08 some more. > This seems to work for both: > $ tar --format=ustar -c /dev/null > /dev/null > tar: Removing leading '/' from member names > $ gtar --format=ustar -c /dev/null > /dev/null > gtar: Removing leading `/' from member names Cool. LGTM. > I think a Windows system could be using either. BSD tar comes > pre-installed by Microsoft and people often install GNU tools. So I > think we should use File::Spec->devnull() instead of /dev/null, and > Andrew showed that working. Agreed. > Longer term I think we need to tolerate but ignore pax headers. If I > understand the spirit of this long evolution, pax archives are > intended to be acceptable to pre-pax implementations, which implies > that they can't really change the meaning of the bits of the file > contents. I don't buy that. For example, POSIX specifies these allowed fields in an extended header: linkpath The pathname of a link being created to another file, of any type, previously archived. This record shall override the linkname field in the following ustar header block(s). path The pathname of the following file(s). This record shall override the name and prefix fields in the following header block(s). size The size of the file in octets, expressed as a decimal number using digits from the ISO/IEC 646:1991 standard. This record shall override the size field in the following header block(s). GNU tar seems to try hard to ensure that a non-pax-aware tar can extract *something* from a tar file, but it's not guaranteed that the something contains the right data or is located at the right pathname. It looks like the goal is to allow post-processing to pick up the pieces. In any case, this is all completely moot if we don't write code to de-sparse a sparse entry: we will not be able to validate WAL data if the WAL file is missing some pages. So I see little point in having code that tolerates pax headers if it doesn't also do that. regards, tom lane -
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-04-03T00:47:28Z
On Fri, Apr 3, 2026 at 1:11 PM Tom Lane <tgl@sss.pgh.pa.us> wrote: > In any case, this is all completely moot if we don't write code to > de-sparse a sparse entry: we will not be able to validate WAL data > if the WAL file is missing some pages. So I see little point in > having code that tolerates pax headers if it doesn't also do that. Yeah. FWIW I spent a few hours hacking on that the other day and could decode many files, but I now realise that the task was made more difficult by a problem you fixed: without header validation, small mistakes resulted in corruption or went bananas. With that now addressed, I hope I can get it into shape and propose it for the next cycle... For what it's worth, I was just speculating about how one might reasonably handle unrecognised *non-standard* header names, not the POSIX-standardised ones which, you're right, we'd probably need to grok properly. If we assumed reasonable engineering decisions following (what I understood to be) the spirit of pax, maybe we could assume that new non-standard headers either don't affect file contents and thus could be ignored (think: GNU.windows.permissions=...), or do affect file contents but have measures in place to prevent unknown encodings from being exposed to unsuspecting software (think: deathstation.byte=9bit). That's a position we could choose to take, anyway, in the absence of a crystal ball... Fortunately there aren't really many implementations of POSIX left, so it's not like we're dealing with the Fermi Paradox here...
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-04-03T00:59:16Z
On Fri, Apr 3, 2026 at 1:11 PM Tom Lane <tgl@sss.pgh.pa.us> wrote: > Cool. LGTM. Thanks. I'll go ahead and push v2 shortly, which includes Sami's change (with credit), unless you have any better ideas for that bit.
-
Re: pg_waldump: support decoding of WAL inside tarfile
Tom Lane <tgl@sss.pgh.pa.us> — 2026-04-03T01:11:22Z
Thomas Munro <thomas.munro@gmail.com> writes: > Thanks. I'll go ahead and push v2 shortly, which includes Sami's > change (with credit), unless you have any better ideas for that bit. Not really. I wondered for a bit if it was smart to create tar files whose contents would appear root-owned, but it could only matter if somebody extracted such a file as root, and I don't see a plausible pathway for that to happen, since these are merely transient test files. We could dodge that hazard by using, say, "--owner=1" but I'm not sure whether that'd introduce its own problems. In any case, walmethods.c's tar_open_for_write is already filling in zeroes there. regards, tom lane
-
Re: pg_waldump: support decoding of WAL inside tarfile
Nazir Bilal Yavuz <byavuz81@gmail.com> — 2026-04-03T11:37:47Z
Hi, On Thu, 2 Apr 2026 at 21:48, Tom Lane <tgl@sss.pgh.pa.us> wrote: > > FORMAT is one of the following: > gnu GNU tar 1.13.x format > oldgnu GNU format as per tar <= 1.12 > pax POSIX 1003.1-2001 (pax) format > posix same as pax > ustar POSIX 1003.1-1988 (ustar) format > v7 old V7 tar format > ... > *This* tar defaults to: > --format=posix -f- -b20 --quoting-style=escape --rmt-command=/usr/bin/rmt > --rsh-command=/usr/bin/ssh > > So there you have it: pax format by default. This is unlike what > I see on RHEL or Fedora: It seems that the problem also applies to OpenBSD [1]: -F format Specify the output archive format, with the default format being pax. tar currently supports the following formats: OpenBSD CI tasks started to fail [2] after bc30c704ad with the errors: ``` Listing only the last 100 lines from a long log. # at /home/postgres/postgres/src/bin/pg_waldump/t/001_basic.pl line 440. # got: 'pg_waldump: error: pax extensions to tar format are not supported # Failed test 'corrupt backup fails verification: extra_file: matches' # at /home/postgres/postgres/src/bin/pg_verifybackup/t/003_corruption.pl line 198. # 'pg_verifybackup: error: pax extensions to tar format are not supported Summary of Failures: 239/381 postgresql:pg_waldump / pg_waldump/001_basic ERROR 18.58s exit status 84 225/381 postgresql:pg_verifybackup / pg_verifybackup/003_corruption ERROR 45.12s exit status 8 ``` I also tried Thomas' "v2-0001-Improve-tar-portability-logic-from-ebba64c0" [3] but it didn't fix the problem on OpenBSD [4]. [1] https://man.openbsd.org/tar#F [2] https://cirrus-ci.com/task/5439721360326656 [3] https://postgr.es/m/CA%2BhUKGLMkv_fnGXzVRO8qbx5uHs-qMn151GTJYCfn9w1ZamGNg%40mail.gmail.com [4] https://cirrus-ci.com/task/5602126958690304 -- Regards, Nazir Bilal Yavuz Microsoft -
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-04-03T14:37:26Z
On Sat, Apr 4, 2026 at 12:38 AM Nazir Bilal Yavuz <byavuz81@gmail.com> wrote: > I also tried Thomas' > "v2-0001-Improve-tar-portability-logic-from-ebba64c0" [3] but it > didn't fix the problem on OpenBSD [4]. Apparently it wants -F ustar, like this. Funny that it passed on the build farm animals though. Oh, it looks like they changed the default fairly recently. https://undeadly.org/cgi?action=article;sid=20240417053301
-
Re: pg_waldump: support decoding of WAL inside tarfile
Sami Imseih <samimseih@gmail.com> — 2026-04-03T14:59:19Z
Hi, > On Sat, Apr 4, 2026 at 12:38 AM Nazir Bilal Yavuz <byavuz81@gmail.com> wrote: > > I also tried Thomas' > > "v2-0001-Improve-tar-portability-logic-from-ebba64c0" [3] but it > > didn't fix the problem on OpenBSD [4]. > > Apparently it wants -F ustar, like this. Funny that it passed on the > build farm animals though. Oh, it looks like they changed the default > fairly recently. LGTM with just a correction of my earlier comment. < + # ustar format supports UIDs only up to 2^21 (2097151). Override --- > + # ustar format supports UIDs only up to 2^21 - 1 (2097151). Override -- Sami
-
Re: pg_waldump: support decoding of WAL inside tarfile
Nazir Bilal Yavuz <byavuz81@gmail.com> — 2026-04-03T15:30:01Z
Hi, On Fri, 3 Apr 2026 at 17:59, Sami Imseih <samimseih@gmail.com> wrote: > > Hi, > > > On Sat, Apr 4, 2026 at 12:38 AM Nazir Bilal Yavuz <byavuz81@gmail.com> wrote: > > > I also tried Thomas' > > > "v2-0001-Improve-tar-portability-logic-from-ebba64c0" [3] but it > > > didn't fix the problem on OpenBSD [4]. > > > > Apparently it wants -F ustar, like this. Funny that it passed on the > > build farm animals though. Oh, it looks like they changed the default > > fairly recently. > > LGTM with just a correction of my earlier comment. Thanks for the patches! I confirm that both v3 and v4 fix the problem for OpenBSD CI. -- Regards, Nazir Bilal Yavuz Microsoft
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-04-04T01:07:04Z
On Sat, Apr 4, 2026 at 4:30 AM Nazir Bilal Yavuz <byavuz81@gmail.com> wrote: > On Fri, 3 Apr 2026 at 17:59, Sami Imseih <samimseih@gmail.com> wrote: > > > On Sat, Apr 4, 2026 at 12:38 AM Nazir Bilal Yavuz <byavuz81@gmail.com> wrote: > > > > I also tried Thomas' > > > > "v2-0001-Improve-tar-portability-logic-from-ebba64c0" [3] but it > > > > didn't fix the problem on OpenBSD [4]. > > > > > > Apparently it wants -F ustar, like this. Funny that it passed on the > > > build farm animals though. Oh, it looks like they changed the default > > > fairly recently. > > > > LGTM with just a correction of my earlier comment. > > Thanks for the patches! I confirm that both v3 and v4 fix the problem > for OpenBSD CI. Pushed, after testing on an OpenBSD VM and making some corrections: * I'd screwed up the test command line in a way that worked by coincidence ** OpenBSD tar writes to a tape device by default, so use -f /dev/null ** I'd forgotten == 0, so the result was inverted, hiding that screwup * -f /dev/null is a better form for all of them because the default destination is a build option * needed elsif instead of if, or BSD tar finished up getting both --format=ustar and -F ustar * ran perltidy, keeping only the hunks due to this patch CI passes and shows "212 subtests passed" for all five Unixen + Windows/mingw, but only "156 subtests passed" for Windows/MSVC. .cirrus.tasks.yml appears to use the same $TAR for both, namely the system tar, so I think we can say that *this* thing is working, but something else might be wrong with our scripting glue somewhere? The other OSes on our list are AIX and Solaris. From a quick look at their manuals, I don't foresee issues with pax or large UIDs. Hopefully that covers everything!
-
Re: pg_waldump: support decoding of WAL inside tarfile
Thomas Munro <thomas.munro@gmail.com> — 2026-04-10T07:57:56Z
Nitpicking code review for commit b15c1513: +read_archive_wal_page(XLogDumpPrivate *privateInfo, XLogRecPtr targetPagePtr, + Size count, char *readBuff) I thought we agreed to stop using Size for new code? size_t has been around since C89. + pg_fatal("WAL segment \"%s\" in archive \"%s\" is too short: rea d %lld of %lld bytes", + fname, privateInfo->archive_name, + (long long int) (count - nbytes), + (long long int) count); Why cast to long long int? That's the sort of thing we used to have to do for int64 (but no longer), but here it's size_t anyway. %zu has been around since C99. -
Re: pg_waldump: support decoding of WAL inside tarfile
Andrew Dunstan <andrew@dunslane.net> — 2026-04-10T12:57:14Z
On 2026-04-10 Fr 3:57 AM, Thomas Munro wrote: > Nitpicking code review for commit b15c1513: > > +read_archive_wal_page(XLogDumpPrivate *privateInfo, XLogRecPtr targetPagePtr, > + Size count, char *readBuff) > > I thought we agreed to stop using Size for new code? size_t has been > around since C89. Must have missed the memo :-( > > + pg_fatal("WAL segment \"%s\" in archive \"%s\" is too short: rea > d %lld of %lld bytes", > + fname, privateInfo->archive_name, > + (long long int) (count - nbytes), > + (long long int) count); > > Why cast to long long int? That's the sort of thing we used to have > to do for int64 (but no longer), but here it's size_t anyway. %zu has > been around since C99. will fix. Thanks for looking. cheers andrew -- Andrew Dunstan EDB:https://www.enterprisedb.com -
Documenting coding style
Andres Freund <andres@anarazel.de> — 2026-04-10T14:17:44Z
Hi, On April 10, 2026 3:57:56 AM EDT, Thomas Munro <thomas.munro@gmail.com> wrote: >Nitpicking code review for commit b15c1513: > >+read_archive_wal_page(XLogDumpPrivate *privateInfo, XLogRecPtr targetPagePtr, >+ Size count, char *readBuff) > >I thought we agreed to stop using Size for new code? size_t has been >around since C89. We really need to start documenting some of this stuff somewhere. Deciding something a few years ago, deep in a thread, won't actually help anyone but the participants (and maybe not even them) to know about it. I wonder if we should move the coding style section out of sgml into a top-level CODING_STYLE.md or something like that. And then obviously add things like Size being deprecated. Greetings, Andres -- Sent from my Android device with K-9 Mail. Please excuse my brevity.
-
Re: Documenting coding style
Andrew Dunstan <andrew@dunslane.net> — 2026-04-10T14:45:44Z
On 2026-04-10 Fr 10:17 AM, Andres Freund wrote: > Hi, > > On April 10, 2026 3:57:56 AM EDT, Thomas Munro <thomas.munro@gmail.com> wrote: >> Nitpicking code review for commit b15c1513: >> >> +read_archive_wal_page(XLogDumpPrivate *privateInfo, XLogRecPtr targetPagePtr, >> + Size count, char *readBuff) >> >> I thought we agreed to stop using Size for new code? size_t has been >> around since C89. > We really need to start documenting some of this stuff somewhere. Deciding something a few years ago, deep in a thread, won't actually help anyone but the participants (and maybe not even them) to know about it. > > I wonder if we should move the coding style section out of sgml into a top-level CODING_STYLE.md or something like that. > > And then obviously add things like Size being deprecated. > +many. Great idea. cheers andew -- Andrew Dunstan EDB: https://www.enterprisedb.com
-
Re: Documenting coding style
Nathan Bossart <nathandbossart@gmail.com> — 2026-04-10T14:49:42Z
On Fri, Apr 10, 2026 at 10:17:44AM -0400, Andres Freund wrote: > On April 10, 2026 3:57:56 AM EDT, Thomas Munro <thomas.munro@gmail.com> wrote: >> I thought we agreed to stop using Size for new code? size_t has been >> around since C89. > > We really need to start documenting some of this stuff somewhere. > Deciding something a few years ago, deep in a thread, won't actually help > anyone but the participants (and maybe not even them) to know about it.. I certainly didn't know this. There's no comment in c.h, either. > I wonder if we should move the coding style section out of sgml into a > top-level CODING_STYLE.md or something like that. > > And then obviously add things like Size being deprecated. Unless we're going to actually remove the typedef in the near future, I'm not sure I'd support even marking it deprecated. If we're going to keep it around indefinitely, that's just going to become another source of nitpicks when new contributors inevitably copy/paste some code from the aughts. A style page makes the situation a little better, but it's yet another thing that folks have to remember. To be clear, if someone proposed a patch that completely removed all traces of Size, I'd likely support it. There is indeed no reason not to use size_t. (I see that Size has been an alias for size_t since 1998 [0].) But it's also quite heavily used, so I'd be fine with leaving it around and considering it fully supported, too. [0] https://postgr.es/c/0ad5d2a3a8 -- nathan
-
Re: Documenting coding style
Peter Eisentraut <peter@eisentraut.org> — 2026-04-15T18:10:59Z
On 10.04.26 16:49, Nathan Bossart wrote: >> I wonder if we should move the coding style section out of sgml into a >> top-level CODING_STYLE.md or something like that. >> >> And then obviously add things like Size being deprecated. > Unless we're going to actually remove the typedef in the near future, I'm > not sure I'd support even marking it deprecated. If we're going to keep it > around indefinitely, that's just going to become another source of nitpicks > when new contributors inevitably copy/paste some code from the aughts. A > style page makes the situation a little better, but it's yet another thing > that folks have to remember. In this case, if one wanted to do something, one should at least enhance the code comment for Size, which is currently very bare and doesn't explain why the type exists. As some counterexamples, some of which I wrote: /* * Pointer * Variable holding address of any memory resident object. * (obsolescent; use void * or char *) */ typedef void *Pointer; /* Historical names for types in <stdint.h>. */ typedef int8_t int8; ... * We require C11 and C++11, so static_assert() is expected to be there. * StaticAssertDecl() was previously used for portability, but it's now just a * plain wrapper and doesn't need to be used in new code.