(new thread)

On Wed, Jan 7, 2026 at 8:20 PM Oleg Tkachenko <[email protected]> wrote:
>
> Hello Robert,
>
> I checked the VM fork file and found that its incremental version has a wrong
> block number in the header:
>
> ```
> xxd -l 12 INCREMENTAL.16384_vm
> 0d1f aed3 0100 0000 0000 0200  <--- 131072 blocks (1 GB)
>                                    ^^^^  ^^^^
> ```
>
> This value can only come from the WAL summaries, so I checked them too.
> One of the summary files contains:
>
> ```
> TS 1663, DB 5, REL 16384, FORK main: limit 131073
> TS 1663, DB 5, REL 16384, FORK vm: limit 131073
> TS 1663, DB 5, REL 16384, FORK vm: block 4
>
> ```
>
> Both forks have the same limit, which looks wrong.
> So I checked the WAL files to see what really happened with the VM fork.
> I did not find any “truncate" records for the VM file.
> I only found this record for the main fork
> (actually, the fork isn’t mentioned at all):
>
> ```
> rmgr: Storage  len (rec/tot): 46/46, tx: 759, lsn: 0/4600D318,
>  prev 0/4600B2C8, desc: TRUNCATE base/5/16384 to 131073 blocks flags 7
> ```
>
> This suggests that the WAL summarizer may be mixing up information between
> relation forks.
>

$subject found, while discussing another bug in the incremental backup
feature [1].

The issue is when a relation spanning multiple segments (e.g., > 1 GB)
is truncated down to a single segment (or a smaller size) via VACUUM.
This action generates an SMGR_TRUNCATE_ALL WAL record. When a
subsequent incremental backup is taken and then processed by
pg_combinebackup, the resulting Visibility Map (VM) fork in the
combined backup is reconstructed with an incorrect, "insanely high"
size -- the size equal to the main fork.

I have attached a small reproducer by modifying an existing test case
and making it fail so that the file size can be checked. Apply it to
the master branch and run:

cd src/bin/pg_combinebackup/
make check PROVE_TESTS='t/011_ib_truncation.pl'

Backups used for testing will be
"tmp_check/t_011_ib_truncation_primary_data/backup/" directory and
the combined backup result in
"tmp_check/t_011_ib_truncation_node2_data/pgdata/"

If you inspect the relation forks in the final combined backup, you
will see the VM size discrepancy (16384 is the test relation oid):

ll -h tmp_check/t_011_ib_truncation_node2_data/pgdata/base/5/ | grep 16384

-rw-------. 1 amul 1.0G Feb 19 17:10 16384           <----- main fork file
-rw-------. 1 amul 8.0K Feb 19 17:10 16384.1
-rw-------. 1 amul 280K Feb 19 17:10 16384_fsm
-rw-------. 1 amul 1.0G Feb 19 17:10 16384_vm.   <----- vm fork file (1 GB)

The reason, as Oleg explained in the same thread [1], is that the
summary file recorded an incorrect size limit for the VM fork due to a
truncation WAL record with the SMGR_TRUNCATE_ALL flag.

I think the fix will be to correct the wal summary entry that records
an incorrect truncation limit for the VM fork.  Attached are the
patches: 0001 is a refactoring patch that moves the necessary macro
definitions from visibilitymap.c to visibilitymap.h to correctly
calculate the VM fork limit recorded in the wal summary file, and 0002
provides the actual fix.

1] http://postgr.es/m/[email protected]

--
Regards,
Amul Sul
EDB: http://www.enterprisedb.com

Attachment: repro-incremental_backup.patch.no-cfbot
Description: Binary data

From db65c3a79623ea55e38a31821225fddeeaeae982 Mon Sep 17 00:00:00 2001
From: Amul Sul <[email protected]>
Date: Wed, 4 Mar 2026 09:12:25 +0530
Subject: [PATCH v1 1/2] Refactor: Expose visibility map mapping macros in
 visibilitymap.h.

The logic for mapping heap blocks to visibility map blocks (including the
HEAPBLK_TO_MAPBLOCK macro) is currently internal to visibilitymap.c.
Expose these definitions in the header file to support the requirements
of the WAL summarizer, which needs to perform the same calculation.

Note that the HEAPBLK_TO_MAPBYTE and HEAPBLK_TO_OFFSET macros
don't really need to be exposed, but for consistency and to keep all
the related macro definitions together, these were moved as well.
---
 src/backend/access/heap/visibilitymap.c | 18 ------------------
 src/include/access/visibilitymap.h      | 18 ++++++++++++++++++
 2 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
index 3047bd46def..00aa2dbd31f 100644
--- a/src/backend/access/heap/visibilitymap.c
+++ b/src/backend/access/heap/visibilitymap.c
@@ -101,24 +101,6 @@
 
 /*#define TRACE_VISIBILITYMAP */
 
-/*
- * Size of the bitmap on each visibility map page, in bytes. There's no
- * extra headers, so the whole page minus the standard page header is
- * used for the bitmap.
- */
-#define MAPSIZE (BLCKSZ - MAXALIGN(SizeOfPageHeaderData))
-
-/* Number of heap blocks we can represent in one byte */
-#define HEAPBLOCKS_PER_BYTE (BITS_PER_BYTE / BITS_PER_HEAPBLOCK)
-
-/* Number of heap blocks we can represent in one visibility map page. */
-#define HEAPBLOCKS_PER_PAGE (MAPSIZE * HEAPBLOCKS_PER_BYTE)
-
-/* Mapping from heap block number to the right bit in the visibility map */
-#define HEAPBLK_TO_MAPBLOCK(x) ((x) / HEAPBLOCKS_PER_PAGE)
-#define HEAPBLK_TO_MAPBYTE(x) (((x) % HEAPBLOCKS_PER_PAGE) / HEAPBLOCKS_PER_BYTE)
-#define HEAPBLK_TO_OFFSET(x) (((x) % HEAPBLOCKS_PER_BYTE) * BITS_PER_HEAPBLOCK)
-
 /* Masks for counting subsets of bits in the visibility map. */
 #define VISIBLE_MASK8	(0x55)	/* The lower bit of each bit pair */
 #define FROZEN_MASK8	(0xaa)	/* The upper bit of each bit pair */
diff --git a/src/include/access/visibilitymap.h b/src/include/access/visibilitymap.h
index a0166c5b410..52a3021971e 100644
--- a/src/include/access/visibilitymap.h
+++ b/src/include/access/visibilitymap.h
@@ -27,6 +27,24 @@
 #define VM_ALL_FROZEN(r, b, v) \
 	((visibilitymap_get_status((r), (b), (v)) & VISIBILITYMAP_ALL_FROZEN) != 0)
 
+/*
+ * Size of the bitmap on each visibility map page, in bytes. There's no
+ * extra headers, so the whole page minus the standard page header is
+ * used for the bitmap.
+ */
+#define MAPSIZE (BLCKSZ - MAXALIGN(SizeOfPageHeaderData))
+
+/* Number of heap blocks we can represent in one byte */
+#define HEAPBLOCKS_PER_BYTE (BITS_PER_BYTE / BITS_PER_HEAPBLOCK)
+
+/* Number of heap blocks we can represent in one visibility map page. */
+#define HEAPBLOCKS_PER_PAGE (MAPSIZE * HEAPBLOCKS_PER_BYTE)
+
+/* Mapping from heap block number to the right bit in the visibility map */
+#define HEAPBLK_TO_MAPBLOCK(x) ((x) / HEAPBLOCKS_PER_PAGE)
+#define HEAPBLK_TO_MAPBYTE(x) (((x) % HEAPBLOCKS_PER_PAGE) / HEAPBLOCKS_PER_BYTE)
+#define HEAPBLK_TO_OFFSET(x) (((x) % HEAPBLOCKS_PER_BYTE) * BITS_PER_HEAPBLOCK)
+
 extern bool visibilitymap_clear(Relation rel, BlockNumber heapBlk,
 								Buffer vmbuf, uint8 flags);
 extern void visibilitymap_pin(Relation rel, BlockNumber heapBlk,
-- 
2.47.1

From 1a3017b3e733f8898078c2c9b5178b7eb9b65c9c Mon Sep 17 00:00:00 2001
From: Amul Sul <[email protected]>
Date: Wed, 4 Mar 2026 09:35:50 +0530
Subject: [PATCH v1 2/2] Fix incorrect VM fork truncation limit in WAL summary
 files.

When processing XLOG_SMGR_TRUNCATE, the WAL summarizer was recording
xlrec->blkno (a heap block number) as the limit block for the
VISIBILITYMAP_FORKNUM. However, the block reference table tracks VM
fork blocks by VM page number, not heap block number. Each VM page
covers more than 32K heap blocks, so passing the heap block number
directly inflated the VM limit block enormously.

This incorrect information in the incremental files caused
pg_combinebackup to produce giant 1 GiB Visibility Map files instead
of the small files actually needed.

Fix this by converting xlrec->blkno using HEAPBLK_TO_MAPBLOCK() before
passing it to BlockRefTableSetLimitBlock() to record the correct limit
for the VM fork.
---
 src/backend/postmaster/walsummarizer.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c
index 742137edad6..72c40ca7922 100644
--- a/src/backend/postmaster/walsummarizer.c
+++ b/src/backend/postmaster/walsummarizer.c
@@ -23,6 +23,7 @@
 #include "postgres.h"
 
 #include "access/timeline.h"
+#include "access/visibilitymap.h"
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "access/xlogrecovery.h"
@@ -1351,7 +1352,8 @@ SummarizeSmgrRecord(XLogReaderState *xlogreader, BlockRefTable *brtab)
 									   MAIN_FORKNUM, xlrec->blkno);
 		if ((xlrec->flags & SMGR_TRUNCATE_VM) != 0)
 			BlockRefTableSetLimitBlock(brtab, &xlrec->rlocator,
-									   VISIBILITYMAP_FORKNUM, xlrec->blkno);
+									   VISIBILITYMAP_FORKNUM,
+									   HEAPBLK_TO_MAPBLOCK(xlrec->blkno));
 	}
 }
 
-- 
2.47.1

Reply via email to