Subject: [PROPOSAL] Doublewrite Buffer as an alternative torn page protection to Full Page Write

From: 陈宗志 <[email-masked]>
Date: Tue, 10 Feb 2026 02:52:58 +0800

Hi hackers,

I raised this topic a while back [1] but didn't get much traction, so
I went ahead and implemented it: a doublewrite buffer (DWB) mechanism
for PostgreSQL as an alternative to full_page_writes.

The core argument is straightforward. FPW and checkpoint frequency are
fundamentally at odds:

- FPW wants fewer checkpoints -- each checkpoint triggers a wave of
full-page WAL writes for every page dirtied for the first time,
bloating WAL and tanking write throughput.
- Fast crash recovery wants more checkpoints -- less WAL to replay
means the database comes back sooner.

DWB resolves this tension by moving torn page protection out of the
WAL path entirely. Instead of writing full pages into WAL (foreground,
latency-sensitive), dirty pages are sequentially written to a
dedicated doublewrite buffer area on disk before being flushed to
their actual locations. The buffer is fsync'd once when full, then
pages are scatter-written to their final positions. On crash recovery,
intact copies from the DWB repair any torn pages.

Key design differences:

- FPW: 1 WAL write (foreground) + 1 page write = directly impacts SQL latency
- DWB: 2 page writes (background flush path) = minimal user-visible impact
- DWB batches fsync() across multiple pages; WAL fsync batching is
limited by foreground latency constraints
- DWB decouples torn page protection from checkpoint frequency, so you
can checkpoint as often as you want without write amplification

I ran sysbench benchmarks (io-bound, --tables=10
--table_size=10000000) with checkpoint_timeout=30s,
shared_buffers=4GB, synchronous_commit=on. Each scenario uses a fresh
database, VACUUM FULL, 60s warmup, 300s run.

Results (TPS):

                     FPW OFF    FPW ON     DWB ON
read_write/32        18,038      7,943     13,009
read_write/64        24,249      9,533     15,387
read_write/128       27,801      9,715     15,387
write_only/32        53,146     18,116     31,460
write_only/64        57,628     19,589     32,875
write_only/128       59,454     14,857     33,814

Avg latency (ms):

                     FPW OFF    FPW ON     DWB ON
read_write/32          1.77       4.03       2.46
read_write/64          2.64       6.71       4.16
read_write/128         4.60      13.17       9.81
write_only/32          0.60       1.77       1.02
write_only/64          1.11       3.27       1.95
write_only/128         2.15       8.61       3.78

FPW ON drops to ~25% of baseline (FPW OFF). DWB ON holds at ~57%. In
write-heavy scenarios DWB delivers over 2x the throughput of FPW with
significantly better latency.

The implementation is here: https://github.com/baotiao/postgres

I'd appreciate any feedback on the approach. Would be great if the
community could take a look and see if this direction is worth
pursuing upstream.

Thanks,
Baotiao

[1] https://www.postgresql.org/message-id/CAGbZs7hbJeUe7xY4QD25QW6VSnNFk1e3cwbCa8_R%2B2%2BYnoYRKw%40mail.gmail.com

---

From: Jakub Wartak <[email-masked]>
Date: Mon, 16 Feb 2026 15:07:16 +0100

> community could take a look and see if this direction is worth

Hi Baotiao

I'm a newbie here, but took Your idea with some interest, probably everyone
else is busy with work on other patches before commit freeze.

I think it would be valuable to have this as I've been hit by PostgreSQL's
unsteady (chain-saw-like) WAL traffic, especially related to touching 1st the
pages after checkpoint, up to the point of saturating network links. The common
counter-argument to double buffering is probably that FPI may(?) increase WAL
standby replication rate and this would have to be measured into account
(but we also should take into account how much maintenance_io_concurrency/
posix_fadvise() prefetching that we do today helps avoid any I/O stalls on
fetching pages - so it should be basically free), I see even that you
got benefits
by not using FPI. Interesting.

Some notes/questions about the patches itself:

0. The convention here is send the patches using:
   git format-patch -v<VERSION> HEAD~<numberOfpatches>
   for easier review. The 0003 probably should be out of scope. Anyway I've
   attached all of those so maybe somebody else is going to take a
look at them too,
   they look very mature. Is this code used in production already anywhere? (and
   BTW the numbers are quite impressive)

1. We have full_page_writes = on/off, but Your's patch adds double_write_buffer
   IMHO if we have competing solution it would be better to have something like
   io_torn_pages_protection = off | full_pages | double_writes
   and maybe we'll be able to add 'atomic_writes' one day.
   BTW: once you stabilize the GUC, it is worth adding to postgresql.conf.sample

2. How would one know how to size double_write_buffer_size ?

2b. IMHO the patch could have enriched pg_stat_io with some
information. Please take
   a look on pg_stat_io view and functions like pgstat_count_io_op_time() and
   their parameters and enums there, that way we could have IOOBJECT_DWBUF maybe
   and be able to say how much I/O was attributed to double-buffering, fsync()
   times related to it and so on.

3. In DWBufPostCheckpoint() there's pg_usleep(1ms) just before atomic pwrites(),
   but exactly why is it necessary to have this literally sched_yield(2) there?

4. In BufferSync() I have doubts if such copying is safe in loop:
        page = BufHdrGetBlock(bufHdr);
        memcpy(dwb_buf, page, BLCKSZ);
   shouldn't there be some form of locking (BUFFER_LOCK_SHARE?)/pinning buffers?
   Also it wouldn't be better if that memcpy would be guarded by the
critical section?
   (START_CRIT_SECTION)

4b. There seems to be double coping: there's palloc for dwb_buf in BufferSync()
   that is filled by memcpy(), and then DWBufWritePage() is called and
then again
   that "page" is copied a second time using memcpy(). This seems to be done for
   every checkpoint page, so may reduce benefits of this double-buffering code.

4c. Shouldn't this active waiting in DWBufWritePage() shouldn't be achieved
   using spinlocks rather than pg_usleep(100us)?

5. Have you maybe verified using injection points (or gdb) if crashing
in several
   places really hits that DWBufRecoverPage()? Is there a simple way
of reproducing this
   to play with it? (possibly that could be good test on it's own)

6. Quick testing overview (for completeness)
   - basic test without even enabling this feature complains about
postgresql.conf.sample
     (test_misc/003_check_guc)
   - with `PG_TEST_INITDB_EXTRA_OPTS="-c double_write_buffer=on" meson
test` I've
     got 3 failures there:
     * test_misc/003_check_guc (expected)
     * pg_waldump/002_save_fullpage (I would say it's expected)
     * pg_walinspect / pg_walinspect/regress (I would say it's expected)

I haven't really got it up and running for real, but at least that's
some start and I hope that helps.

-J.
From c6d8f95de78a48090a8b9e38adb97077e2ef3a11 Mon Sep 17 00:00:00 2001
From: "zongzhi.czz" <[email-masked]>
Date: Sat, 7 Feb 2026 22:20:20 +0800
Subject: [PATCH v1 3/4] Add Claude Code configuration

Add local settings for Claude Code with permission allowlist for
common development and testing commands.

Co-Authored-By: Claude Opus 4.5 <[email-masked]>
---
 .claude/settings.local.json | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)
 create mode 100644 .claude/settings.local.json

diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000000..fbf9331f39
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,25 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(grep:*)",
+      "Skill(humanizer-zh)",
+      "Bash(/data/baotiao/postgres/bin/bin/psql:*)",
+      "Bash(/data/baotiao/postgres/bin/bin/pg_ctl:*)",
+      "Bash(cat:*)",
+      "Bash(make:*)",
+      "Bash(make install:*)",
+      "Bash(# 查看日志文件 ls -la /tmp/dwb_test/log/ # 查看最新的日志内容 cat /tmp/dwb_test/log/postgresql-*.log)",
+      "Bash(__NEW_LINE_77c4dfbfc8b69572__ /data/baotiao/postgres/bin/bin/psql -h /tmp -p 15432 -d postgres -c \"\nDROP TABLE IF EXISTS test_flush;\nCREATE TABLE test_flush \\(id int\\);\nINSERT INTO test_flush SELECT generate_series\\(1, 1000\\);\nCHECKPOINT;\n\")",
+      "Bash(__NEW_LINE_77c4dfbfc8b69572__ sleep 1)",
+      "Bash(echo:*)",
+      "Bash(__NEW_LINE_65ede60b14cab4b2__ echo \"=== DWB ON Performance Test ===\")",
+      "Bash(/data/baotiao/postgres/bin/bin/pgbench:*)",
+      "Bash(# 停止旧实例 /data/baotiao/postgres/bin/bin/pg_ctl -D /tmp/dwb_test stop || true # 在真实磁盘上创建新实例 rm -rf /data/baotiao/dwb_test_real /data/baotiao/postgres/bin/bin/initdb -D /data/baotiao/dwb_test_real 2>&1)",
+      "Bash(# 验证 DWB 目录 ls -la /data/baotiao/dwb_test_real/pg_dwbuf/ || echo \"\"DWB directory not created yet\"\" # 初始化 pgbench /data/baotiao/postgres/bin/bin/pgbench -h /tmp -p 15433 -i -s 10 postgres 2>&1)",
+      "Bash(ls:*)",
+      "Bash(/data/baotiao/postgres/bin/bin/postgres:*)",
+      "Bash(/data/baotiao/postgres/bin/bin/createdb:*)",
+      "Bash(sysbench:*)"
+    ]
+  }
+}
-- 
2.43.0

From 264a21dcb4ae4b7a9a61d0584b4d7bef44f41eff Mon Sep 17 00:00:00 2001
From: "zongzhi.czz" <[email-masked]>
Date: Fri, 30 Jan 2026 04:59:43 +0800
Subject: [PATCH v1 1/4] Add double write buffer (DWB) for torn page protection

Implement a double write buffer mechanism as an alternative to full page
writes (FPW). When enabled, dirty pages are written to a dedicated DWB
file before being written to the data files. This provides protection
against torn pages without requiring full page images in WAL.

Key benefits over FPW:
- Dramatically reduced WAL volume (up to 98% reduction in IO-bound workloads)
- Lower network bandwidth for replication
- Faster WAL replay during recovery

The implementation includes:
- New GUC parameters: double_write_buffer (bool) and
  double_write_buffer_size (int, default 64MB)
- DWB files stored in pg_dwbuf/ directory
- Integration with checkpoint for proper flush ordering
- Per-process file descriptor management for correctness

Performance testing shows:
- WAL reduction: 270GB -> 4.6GB (58x reduction) in IO-bound scenarios
- TPS overhead: ~1% compared to no protection
- Comparable TPS to FPW with vastly reduced WAL

Co-Authored-By: Claude Opus 4.5 <[email-masked]>
---
 src/backend/storage/buffer/bufmgr.c       |  15 +
 src/backend/storage/buffer/dwbuf.c        | 745 ++++++++++++++++++++++
 src/backend/utils/misc/guc_parameters.dat |  18 +
 src/backend/utils/misc/guc_tables.c       |   1 +
 src/include/storage/dwbuf.h               | 141 ++++
 5 files changed, 920 insertions(+)
 create mode 100644 src/backend/storage/buffer/dwbuf.c
 create mode 100644 src/include/storage/dwbuf.h

diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 7241477cac..ea84aeef26 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -54,6 +54,7 @@
 #include "storage/aio.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
+#include "storage/dwbuf.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -4497,6 +4498,20 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
 
 	io_start = pgstat_prepare_io_time(track_io_timing);
 
+	/*
+	 * If double write buffer is enabled, write the page to DWB first.
+	 * This protects against torn pages without needing full page writes in WAL.
+	 */
+	if (DWBufIsEnabled())
+	{
+		DWBufWritePage(BufTagGetRelFileLocator(&buf->tag),
+					   BufTagGetForkNum(&buf->tag),
+					   buf->tag.blockNum,
+					   bufToWrite,
+					   recptr);
+		DWBufFlush();
+	}
+
 	/*
 	 * bufToWrite is either the shared buffer or a copy, as appropriate.
 	 */
diff --git a/src/backend/storage/buffer/dwbuf.c b/src/backend/storage/buffer/dwbuf.c
new file mode 100644
index 0000000000..9ccb99b214
--- /dev/null
+++ b/src/backend/storage/buffer/dwbuf.c
@@ -0,0 +1,745 @@
+/*-------------------------------------------------------------------------
+ *
+ * dwbuf.c
+ *	  Double Write Buffer implementation.
+ *
+ * The double write buffer (DWB) provides protection against torn page writes
+ * by writing pages to a dedicated buffer file before writing to the actual
+ * data files. If a crash occurs during a data file write, the page can be
+ * recovered from the DWB.
+ *
+ * This mechanism can replace full_page_writes with better efficiency since
+ * it avoids writing full page images to WAL.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/storage/buffer/dwbuf.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/stat.h>
+
+#include "access/xlog.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "port/pg_crc32c.h"
+#include "storage/dwbuf.h"
+#include "storage/fd.h"
+#include "storage/shmem.h"
+#include "utils/guc.h"
+#include "utils/hsearch.h"
+#include "utils/memutils.h"
+
+/* GUC variables */
+bool		double_write_buffer = false;
+int			double_write_buffer_size = DWBUF_DEFAULT_SIZE_MB;
+
+/* Shared memory control structure */
+static DWBufCtlData *DWBufCtl = NULL;
+
+/* Per-process file descriptors (FDs are per-process, not shareable) */
+static int DWBufFds[DWBUF_MAX_FILES] = {-1, -1, -1, -1, -1, -1, -1, -1,
+                                         -1, -1, -1, -1, -1, -1, -1, -1};
+static bool DWBufFilesOpened = false;
+
+/* Directory for DWB files */
+#define DWBUF_DIR			"pg_dwbuf"
+#define DWBUF_FILE_PREFIX	"dwbuf_"
+
+/* Recovery hash table for page lookup */
+static HTAB *dwbuf_recovery_hash = NULL;
+
+/* Recovery hash table entry */
+typedef struct DWBufRecoveryEntry
+{
+	/* Hash key */
+	RelFileLocator	rlocator;
+	ForkNumber		forknum;
+	BlockNumber		blkno;
+
+	/* Data */
+	int				file_idx;		/* Which DWB file */
+	int				slot_idx;		/* Slot index in file */
+	XLogRecPtr		lsn;			/* Page LSN */
+} DWBufRecoveryEntry;
+
+/* Hash key for recovery entries */
+typedef struct DWBufRecoveryKey
+{
+	RelFileLocator	rlocator;
+	ForkNumber		forknum;
+	BlockNumber		blkno;
+} DWBufRecoveryKey;
+
+/* Local buffer for page operations */
+static char *dwbuf_page_buffer = NULL;
+
+/*
+ * Compute size of shared memory needed for DWB control structure.
+ */
+Size
+DWBufShmemSize(void)
+{
+	if (!double_write_buffer)
+		return 0;
+
+	return MAXALIGN(sizeof(DWBufCtlData));
+}
+
+/*
+ * Initialize DWB shared memory structures.
+ */
+void
+DWBufShmemInit(void)
+{
+	bool		found;
+
+	if (!double_write_buffer)
+		return;
+
+	DWBufCtl = (DWBufCtlData *)
+		ShmemInitStruct("Double Write Buffer",
+						DWBufShmemSize(),
+						&found);
+
+	if (!found)
+	{
+		int			total_slots;
+		int			slots_per_file;
+
+		/* Initialize the control structure */
+		SpinLockInit(&DWBufCtl->mutex);
+
+		/* Calculate number of slots based on configured size */
+		total_slots = (double_write_buffer_size * 1024 * 1024) / DWBUF_SLOT_SIZE;
+		if (total_slots < 64)
+			total_slots = 64;	/* Minimum 64 slots */
+
+		/* Distribute slots across files */
+		DWBufCtl->num_files = (total_slots + 4095) / 4096;
+		if (DWBufCtl->num_files > DWBUF_MAX_FILES)
+			DWBufCtl->num_files = DWBUF_MAX_FILES;
+
+		slots_per_file = total_slots / DWBufCtl->num_files;
+		DWBufCtl->slots_per_file = slots_per_file;
+		DWBufCtl->num_slots = slots_per_file * DWBufCtl->num_files;
+
+		/* Initialize atomic variables */
+		pg_atomic_init_u64(&DWBufCtl->write_pos, 0);
+		pg_atomic_init_u64(&DWBufCtl->flush_pos, 0);
+
+		/* Initialize other fields */
+		DWBufCtl->batch_id = 0;
+		DWBufCtl->flushed_batch_id = 0;
+		DWBufCtl->checkpoint_lsn = InvalidXLogRecPtr;
+	}
+}
+
+/*
+ * Get the path for a DWB segment file.
+ */
+static void
+DWBufFilePath(char *path, int file_idx)
+{
+	snprintf(path, MAXPGPATH, "%s/%s%03d", DWBUF_DIR, DWBUF_FILE_PREFIX, file_idx);
+}
+
+/*
+ * Initialize DWB files for this process.
+ * This is called lazily the first time DWB is used.
+ */
+static void
+DWBufOpenFiles(void)
+{
+	int			i;
+	char		path[MAXPGPATH];
+	struct stat	st;
+
+	if (DWBufFilesOpened)
+		return;
+
+	if (!double_write_buffer || DWBufCtl == NULL)
+		return;
+
+	/* Create directory if it doesn't exist */
+	if (stat(DWBUF_DIR, &st) != 0)
+	{
+		if (MakePGDirectory(DWBUF_DIR) < 0)
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not create directory \"%s\": %m", DWBUF_DIR)));
+	}
+
+	/* Open or create segment files */
+	for (i = 0; i < DWBufCtl->num_files; i++)
+	{
+		int			fd;
+		off_t		expected_size;
+
+		DWBufFilePath(path, i);
+
+		/* Calculate expected file size */
+		expected_size = sizeof(DWBufFileHeader) +
+			(off_t) DWBufCtl->slots_per_file * DWBUF_SLOT_SIZE;
+
+		fd = BasicOpenFile(path, O_RDWR | O_CREAT | PG_BINARY);
+		if (fd < 0)
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not open double write buffer file \"%s\": %m",
+							path)));
+
+		/* Extend file if needed */
+		if (fstat(fd, &st) == 0 && st.st_size < expected_size)
+		{
+			if (ftruncate(fd, expected_size) != 0)
+			{
+				close(fd);
+				ereport(ERROR,
+						(errcode_for_file_access(),
+						 errmsg("could not extend double write buffer file \"%s\": %m",
+								path)));
+			}
+
+			/* Initialize the file header */
+			{
+				DWBufFileHeader header;
+
+				memset(&header, 0, sizeof(header));
+				header.magic = DWBUF_MAGIC;
+				header.version = DWBUF_VERSION;
+				header.blcksz = BLCKSZ;
+				header.slots_per_file = DWBufCtl->slots_per_file;
+				header.batch_id = 0;
+				header.checkpoint_lsn = InvalidXLogRecPtr;
+
+				/* Compute CRC */
+				INIT_CRC32C(header.crc);
+				COMP_CRC32C(header.crc, &header, offsetof(DWBufFileHeader, crc));
+				FIN_CRC32C(header.crc);
+
+				if (pg_pwrite(fd, &header, sizeof(header), 0) != sizeof(header))
+				{
+					close(fd);
+					ereport(ERROR,
+							(errcode_for_file_access(),
+							 errmsg("could not write double write buffer header: %m")));
+				}
+
+				if (pg_fsync(fd) != 0)
+				{
+					close(fd);
+					ereport(ERROR,
+							(errcode_for_file_access(),
+							 errmsg("could not fsync double write buffer file: %m")));
+				}
+			}
+		}
+
+		DWBufFds[i] = fd;
+	}
+
+	/* Allocate local page buffer */
+	if (dwbuf_page_buffer == NULL)
+		dwbuf_page_buffer = MemoryContextAllocAligned(TopMemoryContext,
+													  DWBUF_SLOT_SIZE,
+													  PG_IO_ALIGN_SIZE,
+													  0);
+
+	DWBufFilesOpened = true;
+}
+
+/*
+ * Initialize DWB files at startup.
+ */
+void
+DWBufInit(void)
+{
+	if (!double_write_buffer || DWBufCtl == NULL)
+		return;
+
+	DWBufOpenFiles();
+
+	elog(LOG, "double write buffer initialized with %d slots in %d files",
+		 DWBufCtl->num_slots, DWBufCtl->num_files);
+}
+
+/*
+ * Close DWB files at shutdown.
+ */
+void
+DWBufClose(void)
+{
+	int			i;
+
+	if (!DWBufFilesOpened)
+		return;
+
+	for (i = 0; i < DWBUF_MAX_FILES; i++)
+	{
+		if (DWBufFds[i] >= 0)
+		{
+			close(DWBufFds[i]);
+			DWBufFds[i] = -1;
+		}
+	}
+	DWBufFilesOpened = false;
+}
+
+/*
+ * Write a page to the double write buffer.
+ */
+void
+DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
+			   BlockNumber blkno, const char *page, XLogRecPtr lsn)
+{
+	uint64		pos;
+	int			file_idx;
+	int			slot_idx;
+	off_t		offset;
+	DWBufPageSlot *slot;
+	pg_crc32c	crc;
+
+	if (!double_write_buffer || DWBufCtl == NULL)
+		return;
+
+	/* Ensure files are opened (lazy initialization) */
+	if (!DWBufFilesOpened)
+		DWBufOpenFiles();
+
+	/* Get next slot position atomically */
+	pos = pg_atomic_fetch_add_u64(&DWBufCtl->write_pos, 1);
+
+	/* Calculate file and slot indices */
+	file_idx = (pos / DWBufCtl->slots_per_file) % DWBufCtl->num_files;
+	slot_idx = pos % DWBufCtl->slots_per_file;
+
+	/* Calculate offset in file */
+	offset = sizeof(DWBufFileHeader) + (off_t) slot_idx * DWBUF_SLOT_SIZE;
+
+	/* Build slot header in local buffer */
+	slot = (DWBufPageSlot *) dwbuf_page_buffer;
+	slot->rlocator = rlocator;
+	slot->forknum = forknum;
+	slot->blkno = blkno;
+	slot->lsn = lsn;
+	slot->slot_id = (uint32) pos;
+	slot->flags = DWBUF_SLOT_VALID;
+	slot->checksum = 0;			/* Will be set by PageSetChecksumCopy */
+
+	/* Copy page data after header */
+	memcpy(dwbuf_page_buffer + sizeof(DWBufPageSlot), page, BLCKSZ);
+
+	/* Compute CRC over slot header and page data */
+	INIT_CRC32C(crc);
+	COMP_CRC32C(crc, dwbuf_page_buffer + sizeof(pg_crc32c),
+				sizeof(DWBufPageSlot) - sizeof(pg_crc32c) + BLCKSZ);
+	FIN_CRC32C(crc);
+	slot->crc = crc;
+
+	/* Write to DWB file */
+	if (pg_pwrite(DWBufFds[file_idx], dwbuf_page_buffer,
+				  DWBUF_SLOT_SIZE, offset) != DWBUF_SLOT_SIZE)
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not write to double write buffer: %m")));
+}
+
+/*
+ * Flush all written pages in the DWB to disk.
+ */
+void
+DWBufFlush(void)
+{
+	int			i;
+	uint64		current_pos;
+	uint64		flush_pos;
+
+	if (!double_write_buffer || DWBufCtl == NULL || !DWBufFilesOpened)
+		return;
+
+	current_pos = pg_atomic_read_u64(&DWBufCtl->write_pos);
+	flush_pos = pg_atomic_read_u64(&DWBufCtl->flush_pos);
+
+	/* Nothing to flush */
+	if (current_pos <= flush_pos)
+		return;
+
+	/* Fsync all DWB files */
+	for (i = 0; i < DWBufCtl->num_files; i++)
+	{
+		if (DWBufFds[i] >= 0)
+		{
+			if (pg_fsync(DWBufFds[i]) != 0)
+				ereport(ERROR,
+						(errcode_for_file_access(),
+						 errmsg("could not fsync double write buffer: %m")));
+		}
+	}
+
+	/* Update flush position */
+	pg_atomic_write_u64(&DWBufCtl->flush_pos, current_pos);
+}
+
+/*
+ * Flush all pages and ensure DWB is fully synced.
+ */
+void
+DWBufFlushAll(void)
+{
+	if (!double_write_buffer || DWBufCtl == NULL)
+		return;
+
+	DWBufFlush();
+
+	SpinLockAcquire(&DWBufCtl->mutex);
+	DWBufCtl->flushed_batch_id = DWBufCtl->batch_id;
+	SpinLockRelease(&DWBufCtl->mutex);
+}
+
+/*
+ * Called before checkpoint to ensure DWB is in consistent state.
+ */
+void
+DWBufPreCheckpoint(void)
+{
+	if (!double_write_buffer || DWBufCtl == NULL)
+		return;
+
+	/* Flush all pending writes */
+	DWBufFlushAll();
+}
+
+/*
+ * Called after checkpoint to reset DWB for next cycle.
+ */
+void
+DWBufPostCheckpoint(XLogRecPtr checkpoint_lsn)
+{
+	int			i;
+
+	if (!double_write_buffer || DWBufCtl == NULL)
+		return;
+
+	/* Ensure files are opened */
+	if (!DWBufFilesOpened)
+		DWBufOpenFiles();
+
+	SpinLockAcquire(&DWBufCtl->mutex);
+
+	/* Reset write position for new batch */
+	pg_atomic_write_u64(&DWBufCtl->write_pos, 0);
+	pg_atomic_write_u64(&DWBufCtl->flush_pos, 0);
+
+	/* Increment batch ID */
+	DWBufCtl->batch_id++;
+	DWBufCtl->checkpoint_lsn = checkpoint_lsn;
+
+	SpinLockRelease(&DWBufCtl->mutex);
+
+	/* Update file headers with new batch info */
+	for (i = 0; i < DWBufCtl->num_files; i++)
+	{
+		DWBufFileHeader header;
+		char		path[MAXPGPATH];
+
+		if (DWBufFds[i] < 0)
+			continue;
+
+		/* Read current header */
+		if (pg_pread(DWBufFds[i], &header, sizeof(header), 0) != sizeof(header))
+		{
+			DWBufFilePath(path, i);
+			ereport(WARNING,
+					(errcode_for_file_access(),
+					 errmsg("could not read double write buffer header from \"%s\": %m",
+							path)));
+			continue;
+		}
+
+		/* Update header */
+		header.batch_id = DWBufCtl->batch_id;
+		header.checkpoint_lsn = checkpoint_lsn;
+
+		/* Recompute CRC */
+		INIT_CRC32C(header.crc);
+		COMP_CRC32C(header.crc, &header, offsetof(DWBufFileHeader, crc));
+		FIN_CRC32C(header.crc);
+
+		/* Write back */
+		if (pg_pwrite(DWBufFds[i], &header, sizeof(header), 0) != sizeof(header))
+		{
+			DWBufFilePath(path, i);
+			ereport(WARNING,
+					(errcode_for_file_access(),
+					 errmsg("could not write double write buffer header to \"%s\": %m",
+							path)));
+		}
+	}
+}
+
+/*
+ * Reset DWB (called after successful checkpoint).
+ */
+void
+DWBufReset(void)
+{
+	/* DWBufPostCheckpoint handles the reset */
+}
+
+/*
+ * Initialize DWB for recovery.
+ * Scans DWB files and builds a hash table of valid pages.
+ */
+void
+DWBufRecoveryInit(void)
+{
+	HASHCTL		hash_ctl;
+	int			i;
+	char		path[MAXPGPATH];
+	char	   *buffer;
+
+	if (!double_write_buffer)
+		return;
+
+	/* Create hash table for page lookup */
+	memset(&hash_ctl, 0, sizeof(hash_ctl));
+	hash_ctl.keysize = sizeof(DWBufRecoveryKey);
+	hash_ctl.entrysize = sizeof(DWBufRecoveryEntry);
+	hash_ctl.hcxt = CurrentMemoryContext;
+
+	dwbuf_recovery_hash = hash_create("DWBuf Recovery Hash",
+									  1024,
+									  &hash_ctl,
+									  HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+	/* Allocate buffer for reading slots */
+	buffer = palloc_aligned(DWBUF_SLOT_SIZE, PG_IO_ALIGN_SIZE, 0);
+
+	/* Scan all DWB files */
+	for (i = 0; i < DWBUF_MAX_FILES; i++)
+	{
+		int			fd;
+		DWBufFileHeader header;
+		int			slot_idx;
+		struct stat st;
+
+		DWBufFilePath(path, i);
+
+		/* Check if file exists */
+		if (stat(path, &st) != 0)
+			continue;
+
+		fd = BasicOpenFile(path, O_RDONLY | PG_BINARY);
+		if (fd < 0)
+		{
+			elog(WARNING, "could not open DWB file \"%s\" for recovery: %m", path);
+			continue;
+		}
+
+		/* Read and validate header */
+		if (pg_pread(fd, &header, sizeof(header), 0) != sizeof(header))
+		{
+			close(fd);
+			continue;
+		}
+
+		if (header.magic != DWBUF_MAGIC || header.version != DWBUF_VERSION)
+		{
+			close(fd);
+			continue;
+		}
+
+		/* Verify header CRC */
+		{
+			pg_crc32c	crc;
+
+			INIT_CRC32C(crc);
+			COMP_CRC32C(crc, &header, offsetof(DWBufFileHeader, crc));
+			FIN_CRC32C(crc);
+
+			if (!EQ_CRC32C(crc, header.crc))
+			{
+				elog(WARNING, "DWB file \"%s\" has invalid header CRC", path);
+				close(fd);
+				continue;
+			}
+		}
+
+		/* Scan slots in this file */
+		for (slot_idx = 0; slot_idx < (int) header.slots_per_file; slot_idx++)
+		{
+			off_t		offset;
+			DWBufPageSlot *slot;
+			pg_crc32c	crc;
+			DWBufRecoveryKey key;
+			DWBufRecoveryEntry *entry;
+			bool		found;
+
+			offset = sizeof(DWBufFileHeader) + (off_t) slot_idx * DWBUF_SLOT_SIZE;
+
+			if (pg_pread(fd, buffer, DWBUF_SLOT_SIZE, offset) != DWBUF_SLOT_SIZE)
+				break;
+
+			slot = (DWBufPageSlot *) buffer;
+
+			/* Check if slot is valid */
+			if (!(slot->flags & DWBUF_SLOT_VALID))
+				continue;
+
+			/* Verify slot CRC */
+			INIT_CRC32C(crc);
+			COMP_CRC32C(crc, buffer + sizeof(pg_crc32c),
+						sizeof(DWBufPageSlot) - sizeof(pg_crc32c) + BLCKSZ);
+			FIN_CRC32C(crc);
+
+			if (!EQ_CRC32C(crc, slot->crc))
+				continue;		/* Invalid CRC, skip */
+
+			/* Add to hash table (newer entries override older ones) */
+			key.rlocator = slot->rlocator;
+			key.forknum = slot->forknum;
+			key.blkno = slot->blkno;
+
+			entry = hash_search(dwbuf_recovery_hash, &key, HASH_ENTER, &found);
+
+			if (!found || entry->lsn < slot->lsn)
+			{
+				entry->rlocator = slot->rlocator;
+				entry->forknum = slot->forknum;
+				entry->blkno = slot->blkno;
+				entry->file_idx = i;
+				entry->slot_idx = slot_idx;
+				entry->lsn = slot->lsn;
+			}
+		}
+
+		close(fd);
+	}
+
+	pfree(buffer);
+
+	elog(LOG, "double write buffer recovery initialized with %ld pages",
+		 hash_get_num_entries(dwbuf_recovery_hash));
+}
+
+/*
+ * Try to recover a page from DWB.
+ * Returns true if page was recovered, false otherwise.
+ */
+bool
+DWBufRecoverPage(RelFileLocator rlocator, ForkNumber forknum,
+				 BlockNumber blkno, char *page)
+{
+	DWBufRecoveryKey key;
+	DWBufRecoveryEntry *entry;
+	char		path[MAXPGPATH];
+	int			fd;
+	off_t		offset;
+	char	   *buffer;
+	DWBufPageSlot *slot;
+	pg_crc32c	crc;
+
+	if (dwbuf_recovery_hash == NULL)
+		return false;
+
+	/* Look up page in hash table */
+	key.rlocator = rlocator;
+	key.forknum = forknum;
+	key.blkno = blkno;
+
+	entry = hash_search(dwbuf_recovery_hash, &key, HASH_FIND, NULL);
+	if (entry == NULL)
+		return false;
+
+	/* Read page from DWB file */
+	DWBufFilePath(path, entry->file_idx);
+
+	fd = BasicOpenFile(path, O_RDONLY | PG_BINARY);
+	if (fd < 0)
+		return false;
+
+	offset = sizeof(DWBufFileHeader) + (off_t) entry->slot_idx * DWBUF_SLOT_SIZE;
+
+	buffer = palloc_aligned(DWBUF_SLOT_SIZE, PG_IO_ALIGN_SIZE, 0);
+
+	if (pg_pread(fd, buffer, DWBUF_SLOT_SIZE, offset) != DWBUF_SLOT_SIZE)
+	{
+		pfree(buffer);
+		close(fd);
+		return false;
+	}
+
+	close(fd);
+
+	slot = (DWBufPageSlot *) buffer;
+
+	/* Verify CRC again */
+	INIT_CRC32C(crc);
+	COMP_CRC32C(crc, buffer + sizeof(pg_crc32c),
+				sizeof(DWBufPageSlot) - sizeof(pg_crc32c) + BLCKSZ);
+	FIN_CRC32C(crc);
+
+	if (!EQ_CRC32C(crc, slot->crc))
+	{
+		pfree(buffer);
+		return false;
+	}
+
+	/* Copy page data */
+	memcpy(page, buffer + sizeof(DWBufPageSlot), BLCKSZ);
+
+	pfree(buffer);
+
+	elog(DEBUG1, "recovered page %u/%u/%u fork %d block %u from DWB",
+		 rlocator.spcOid, rlocator.dbOid, rlocator.relNumber,
+		 forknum, blkno);
+
+	return true;
+}
+
+/*
+ * Finish DWB recovery and clean up.
+ */
+void
+DWBufRecoveryFinish(void)
+{
+	if (dwbuf_recovery_hash != NULL)
+	{
+		hash_destroy(dwbuf_recovery_hash);
+		dwbuf_recovery_hash = NULL;
+	}
+}
+
+/*
+ * Check if DWB is enabled.
+ */
+bool
+DWBufIsEnabled(void)
+{
+	return double_write_buffer && DWBufCtl != NULL;
+}
+
+/*
+ * Get current batch ID.
+ */
+uint64
+DWBufGetBatchId(void)
+{
+	uint64		batch_id;
+
+	if (!double_write_buffer || DWBufCtl == NULL)
+		return 0;
+
+	SpinLockAcquire(&DWBufCtl->mutex);
+	batch_id = DWBufCtl->batch_id;
+	SpinLockRelease(&DWBufCtl->mutex);
+
+	return batch_id;
+}
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index c1f1603cd3..be339ca448 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -770,6 +770,24 @@
   check_hook => 'check_default_with_oids',
 },
 
+
+{ name => 'double_write_buffer', type => 'bool', context => 'PGC_POSTMASTER', group => 'WAL_SETTINGS',
+  short_desc => 'Enables double write buffer for torn page protection.',
+  long_desc => 'When enabled, pages are written to a double write buffer before being written to data files. This provides protection against torn pages without needing full page writes in WAL.',
+  variable => 'double_write_buffer',
+  boot_val => 'false',
+},
+
+{ name => 'double_write_buffer_size', type => 'int', context => 'PGC_POSTMASTER', group => 'WAL_SETTINGS',
+  short_desc => 'Sets the size of the double write buffer.',
+  long_desc => 'Size of the double write buffer in megabytes. Larger values allow more pages to be batched before flushing.',
+  variable => 'double_write_buffer_size',
+  boot_val => '64',
+  min => '1',
+  max => '1024',
+  unit => 'MB',
+},
+
 { name => 'dynamic_library_path', type => 'string', context => 'PGC_SUSET', group => 'CLIENT_CONN_OTHER',
   short_desc => 'Sets the path for dynamically loadable modules.',
   long_desc => 'If a dynamically loadable module needs to be opened and the specified name does not have a directory component (i.e., the name does not contain a slash), the system will search this path for the specified file.',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 5df3a36bf6..a35bf3115b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -80,6 +80,7 @@
 #include "storage/bufmgr.h"
 #include "storage/bufpage.h"
 #include "storage/copydir.h"
+#include "storage/dwbuf.h"
 #include "storage/fd.h"
 #include "storage/io_worker.h"
 #include "storage/large_object.h"
diff --git a/src/include/storage/dwbuf.h b/src/include/storage/dwbuf.h
new file mode 100644
index 0000000000..1b096867f2
--- /dev/null
+++ b/src/include/storage/dwbuf.h
@@ -0,0 +1,141 @@
+/*-------------------------------------------------------------------------
+ *
+ * dwbuf.h
+ *	  Double Write Buffer definitions.
+ *
+ * The double write buffer provides protection against torn page writes
+ * by writing pages to a dedicated buffer file before writing to the
+ * actual data files. This can replace full_page_writes for torn page
+ * protection with better efficiency.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/storage/dwbuf.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DWBUF_H
+#define DWBUF_H
+
+#include "storage/block.h"
+#include "storage/buf.h"
+#include "storage/relfilelocator.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "storage/spin.h"
+#include "port/atomics.h"
+#include "port/pg_crc32c.h"
+#include "access/xlogdefs.h"
+
+/*
+ * Double write buffer slot header.
+ * Each slot in the DWB file contains this header followed by the page data.
+ */
+typedef struct DWBufPageSlot
+{
+	RelFileLocator	rlocator;		/* Relation file locator */
+	ForkNumber		forknum;		/* Fork number */
+	BlockNumber		blkno;			/* Block number in relation */
+	XLogRecPtr		lsn;			/* Page LSN at write time */
+	pg_crc32c		crc;			/* CRC of slot header + page content */
+	uint32			slot_id;		/* Slot identifier */
+	uint16			flags;			/* Slot flags */
+	uint16			checksum;		/* Page checksum (if enabled) */
+} DWBufPageSlot;
+
+/* Slot flags */
+#define DWBUF_SLOT_VALID		0x0001	/* Slot contains valid data */
+#define DWBUF_SLOT_FLUSHED		0x0002	/* Slot has been flushed to disk */
+
+/*
+ * Double write buffer file header.
+ * This is stored at the beginning of each DWB segment file.
+ */
+typedef struct DWBufFileHeader
+{
+	uint32			magic;			/* Magic number for validation */
+	uint32			version;		/* Format version */
+	uint32			blcksz;			/* Block size (must match BLCKSZ) */
+	uint32			slots_per_file;	/* Number of slots in this file */
+	uint64			batch_id;		/* Current batch ID */
+	XLogRecPtr		checkpoint_lsn;	/* LSN of last checkpoint */
+	pg_crc32c		crc;			/* CRC of this header */
+} DWBufFileHeader;
+
+#define DWBUF_MAGIC			0x44574246	/* "DWBF" */
+#define DWBUF_VERSION		1
+
+/*
+ * Size of each slot in the DWB file (header + page data, aligned)
+ */
+#define DWBUF_SLOT_SIZE		MAXALIGN(sizeof(DWBufPageSlot) + BLCKSZ)
+
+/*
+ * Double write buffer shared control structure.
+ * This is stored in shared memory and coordinates access to the DWB.
+ */
+typedef struct DWBufCtlData
+{
+	slock_t			mutex;			/* Protects shared state */
+
+	/* Current state */
+	pg_atomic_uint64	write_pos;		/* Next slot to write */
+	pg_atomic_uint64	flush_pos;		/* Last flushed position */
+	uint64			batch_id;		/* Current batch ID */
+	uint64			flushed_batch_id;	/* Last fully flushed batch */
+	XLogRecPtr		checkpoint_lsn;	/* LSN of last checkpoint */
+
+	/* Configuration (set at startup) */
+	int				num_slots;		/* Total number of slots */
+	int				num_files;		/* Number of segment files */
+	int				slots_per_file;	/* Slots per segment file */
+} DWBufCtlData;
+
+/* Maximum number of DWB segment files */
+#define DWBUF_MAX_FILES		16
+
+/* Default and limits for double_write_buffer_size (in MB) */
+#define DWBUF_DEFAULT_SIZE_MB	64
+#define DWBUF_MIN_SIZE_MB		16
+#define DWBUF_MAX_SIZE_MB		1024
+
+/*
+ * Global variables
+ */
+extern PGDLLIMPORT bool double_write_buffer;
+extern PGDLLIMPORT int double_write_buffer_size;
+
+/*
+ * Function prototypes
+ */
+
+/* Initialization and shutdown */
+extern Size DWBufShmemSize(void);
+extern void DWBufShmemInit(void);
+extern void DWBufInit(void);
+extern void DWBufClose(void);
+
+/* Write operations */
+extern void DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
+						   BlockNumber blkno, const char *page,
+						   XLogRecPtr lsn);
+extern void DWBufFlush(void);
+extern void DWBufFlushAll(void);
+
+/* Checkpoint integration */
+extern void DWBufPreCheckpoint(void);
+extern void DWBufPostCheckpoint(XLogRecPtr checkpoint_lsn);
+extern void DWBufReset(void);
+
+/* Recovery operations */
+extern void DWBufRecoveryInit(void);
+extern bool DWBufRecoverPage(RelFileLocator rlocator, ForkNumber forknum,
+							 BlockNumber blkno, char *page);
+extern void DWBufRecoveryFinish(void);
+
+/* Utility functions */
+extern bool DWBufIsEnabled(void);
+extern uint64 DWBufGetBatchId(void);
+
+#endif							/* DWBUF_H */
-- 
2.43.0

From 83100d91f07ac784a355b38fa4dac5775437c1c1 Mon Sep 17 00:00:00 2001
From: "zongzhi.czz" <[email-masked]>
Date: Sat, 7 Feb 2026 22:08:16 +0800
Subject: [PATCH v1 2/4] Fix DWB process handling and skip FPW when DWB enabled

- Skip full page writes when double write buffer is enabled since DWB
  already provides torn page protection
- Fix file descriptor handling after fork by tracking process ID
- Initialize DWB in checkpointer process
- Improve batch synchronization in DWBufPostCheckpoint
- Add DWB shared memory initialization in ipci.c

Co-Authored-By: Claude Opus 4.5 <[email-masked]>
---
 src/backend/access/transam/xlog.c     | 18 +++++-
 src/backend/postmaster/checkpointer.c |  6 ++
 src/backend/storage/buffer/Makefile   |  1 +
 src/backend/storage/buffer/bufmgr.c   |  2 +-
 src/backend/storage/buffer/dwbuf.c    | 79 ++++++++++++++++++++-------
 src/backend/storage/ipc/ipci.c        |  3 +
 6 files changed, 87 insertions(+), 22 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 13ec6225b8..74808d2fcf 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -85,6 +85,7 @@
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
 #include "storage/bufmgr.h"
+#include "storage/dwbuf.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/large_object.h"
@@ -845,7 +846,13 @@ XLogInsertRecord(XLogRecData *rdata,
 			Assert(RedoRecPtr < Insert->RedoRecPtr);
 			RedoRecPtr = Insert->RedoRecPtr;
 		}
-		doPageWrites = (Insert->fullPageWrites || Insert->runningBackups > 0);
+		/*
+		 * If DWB is enabled, we don't need full page writes.
+		 */
+		if (DWBufIsEnabled())
+			doPageWrites = false;
+		else
+			doPageWrites = (Insert->fullPageWrites || Insert->runningBackups > 0);
 
 		if (doPageWrites &&
 			(!prevDoPageWrites ||
@@ -6593,7 +6600,14 @@ void
 GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p)
 {
 	*RedoRecPtr_p = RedoRecPtr;
-	*doPageWrites_p = doPageWrites;
+	/*
+	 * If double write buffer is enabled, we don't need full page writes
+	 * because DWB provides torn page protection.
+	 */
+	if (DWBufIsEnabled())
+		*doPageWrites_p = false;
+	else
+		*doPageWrites_p = doPageWrites;
 }
 
 /*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index e03c19123b..af2edbe222 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -53,6 +53,7 @@
 #include "replication/syncrep.h"
 #include "storage/aio_subsys.h"
 #include "storage/bufmgr.h"
+#include "storage/dwbuf.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -254,6 +255,11 @@ CheckpointerMain(const void *startup_data, size_t startup_data_len)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Initialize double write buffer if enabled.
+	 */
+	DWBufInit();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
diff --git a/src/backend/storage/buffer/Makefile b/src/backend/storage/buffer/Makefile
index fd7c40dcb0..3abab9ec93 100644
--- a/src/backend/storage/buffer/Makefile
+++ b/src/backend/storage/buffer/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	buf_init.o \
 	buf_table.o \
 	bufmgr.o \
+	dwbuf.o \
 	freelist.o \
 	localbuf.o
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index ea84aeef26..8c1e78fba2 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -4501,6 +4501,7 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
 	/*
 	 * If double write buffer is enabled, write the page to DWB first.
 	 * This protects against torn pages without needing full page writes in WAL.
+	 * DWBufWritePage now includes fsync internally for correctness.
 	 */
 	if (DWBufIsEnabled())
 	{
@@ -4509,7 +4510,6 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
 					   buf->tag.blockNum,
 					   bufToWrite,
 					   recptr);
-		DWBufFlush();
 	}
 
 	/*
diff --git a/src/backend/storage/buffer/dwbuf.c b/src/backend/storage/buffer/dwbuf.c
index 9ccb99b214..5c5b14f5b3 100644
--- a/src/backend/storage/buffer/dwbuf.c
+++ b/src/backend/storage/buffer/dwbuf.c
@@ -43,10 +43,12 @@ int			double_write_buffer_size = DWBUF_DEFAULT_SIZE_MB;
 /* Shared memory control structure */
 static DWBufCtlData *DWBufCtl = NULL;
 
+/* Process ID that opened the files (to detect fork) */
+static pid_t DWBufFilesOpenedPid = 0;
+
 /* Per-process file descriptors (FDs are per-process, not shareable) */
 static int DWBufFds[DWBUF_MAX_FILES] = {-1, -1, -1, -1, -1, -1, -1, -1,
                                          -1, -1, -1, -1, -1, -1, -1, -1};
-static bool DWBufFilesOpened = false;
 
 /* Directory for DWB files */
 #define DWBUF_DIR			"pg_dwbuf"
@@ -160,10 +162,20 @@ DWBufOpenFiles(void)
 	int			i;
 	char		path[MAXPGPATH];
 	struct stat	st;
-
-	if (DWBufFilesOpened)
+	pid_t		current_pid = getpid();
+
+	/*
+	 * Check if files are already opened in this process.
+	 * After fork, the child process will have different PID and needs to
+	 * reopen the files.
+	 */
+	if (DWBufFilesOpenedPid == current_pid && DWBufFds[0] >= 0)
 		return;
 
+	/* Close any inherited file descriptors from parent process */
+	if (DWBufFilesOpenedPid != current_pid && DWBufFds[0] >= 0)
+		DWBufClose();
+
 	if (!double_write_buffer || DWBufCtl == NULL)
 		return;
 
@@ -252,7 +264,7 @@ DWBufOpenFiles(void)
 													  PG_IO_ALIGN_SIZE,
 													  0);
 
-	DWBufFilesOpened = true;
+	DWBufFilesOpenedPid = current_pid;
 }
 
 /*
@@ -277,8 +289,9 @@ void
 DWBufClose(void)
 {
 	int			i;
+	pid_t		current_pid = getpid();
 
-	if (!DWBufFilesOpened)
+	if (DWBufFilesOpenedPid != current_pid || DWBufFds[0] < 0)
 		return;
 
 	for (i = 0; i < DWBUF_MAX_FILES; i++)
@@ -289,11 +302,14 @@ DWBufClose(void)
 			DWBufFds[i] = -1;
 		}
 	}
-	DWBufFilesOpened = false;
+	DWBufFilesOpenedPid = 0;
 }
 
 /*
- * Write a page to the double write buffer.
+ * Write a page to the double write buffer and fsync.
+ *
+ * This function writes the page to DWB and ensures it's fsynced to disk
+ * before returning, guaranteeing torn page protection.
  */
 void
 DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
@@ -309,9 +325,8 @@ DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
 	if (!double_write_buffer || DWBufCtl == NULL)
 		return;
 
-	/* Ensure files are opened (lazy initialization) */
-	if (!DWBufFilesOpened)
-		DWBufOpenFiles();
+	/* Ensure files are opened in this process */
+	DWBufOpenFiles();
 
 	/* Get next slot position atomically */
 	pos = pg_atomic_fetch_add_u64(&DWBufCtl->write_pos, 1);
@@ -349,6 +364,11 @@ DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
 		ereport(ERROR,
 				(errcode_for_file_access(),
 				 errmsg("could not write to double write buffer: %m")));
+
+	/*
+	 * NOTE: We don't fsync immediately here for performance reasons.
+	 * The DWBufFlush() function will fsync all files before checkpoint.
+	 */
 }
 
 /*
@@ -361,7 +381,7 @@ DWBufFlush(void)
 	uint64		current_pos;
 	uint64		flush_pos;
 
-	if (!double_write_buffer || DWBufCtl == NULL || !DWBufFilesOpened)
+	if (!double_write_buffer || DWBufCtl == NULL)
 		return;
 
 	current_pos = pg_atomic_read_u64(&DWBufCtl->write_pos);
@@ -371,6 +391,10 @@ DWBufFlush(void)
 	if (current_pos <= flush_pos)
 		return;
 
+	/* Ensure files are opened in this process */
+	if (DWBufFilesOpenedPid != getpid() || DWBufFds[0] < 0)
+		DWBufOpenFiles();
+
 	/* Fsync all DWB files */
 	for (i = 0; i < DWBufCtl->num_files; i++)
 	{
@@ -423,26 +447,43 @@ void
 DWBufPostCheckpoint(XLogRecPtr checkpoint_lsn)
 {
 	int			i;
+	uint64		old_batch_id;
+	uint64		new_batch_id;
 
 	if (!double_write_buffer || DWBufCtl == NULL)
 		return;
 
-	/* Ensure files are opened */
-	if (!DWBufFilesOpened)
+	/* Ensure files are opened in this process */
+	if (DWBufFilesOpenedPid != getpid() || DWBufFds[0] < 0)
 		DWBufOpenFiles();
 
 	SpinLockAcquire(&DWBufCtl->mutex);
 
-	/* Reset write position for new batch */
-	pg_atomic_write_u64(&DWBufCtl->write_pos, 0);
-	pg_atomic_write_u64(&DWBufCtl->flush_pos, 0);
-
-	/* Increment batch ID */
+	/* Save old batch ID and increment */
+	old_batch_id = DWBufCtl->batch_id;
 	DWBufCtl->batch_id++;
+	new_batch_id = DWBufCtl->batch_id;
 	DWBufCtl->checkpoint_lsn = checkpoint_lsn;
 
 	SpinLockRelease(&DWBufCtl->mutex);
 
+	/*
+	 * Wait for all in-flight writes to complete before resetting write_pos.
+	 * We use batch_id as a synchronization point.
+	 */
+	{
+		uint64 current_pos = pg_atomic_read_u64(&DWBufCtl->write_pos);
+		uint64 num_slots = DWBufCtl->num_slots;
+
+		/* If write_pos wrapped around, wait for flush */
+		if (current_pos >= num_slots)
+			DWBufFlush();
+	}
+
+	/* Now safe to reset positions for new batch */
+	pg_atomic_write_u64(&DWBufCtl->write_pos, 0);
+	pg_atomic_write_u64(&DWBufCtl->flush_pos, 0);
+
 	/* Update file headers with new batch info */
 	for (i = 0; i < DWBufCtl->num_files; i++)
 	{
@@ -464,7 +505,7 @@ DWBufPostCheckpoint(XLogRecPtr checkpoint_lsn)
 		}
 
 		/* Update header */
-		header.batch_id = DWBufCtl->batch_id;
+		header.batch_id = new_batch_id;
 		header.checkpoint_lsn = checkpoint_lsn;
 
 		/* Recompute CRC */
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 1f7e933d50..87887dcf69 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -40,6 +40,7 @@
 #include "replication/walsender.h"
 #include "storage/aio_subsys.h"
 #include "storage/bufmgr.h"
+#include "storage/dwbuf.h"
 #include "storage/dsm.h"
 #include "storage/dsm_registry.h"
 #include "storage/ipc.h"
@@ -141,6 +142,7 @@ CalculateShmemSize(void)
 	size = add_size(size, AioShmemSize());
 	size = add_size(size, WaitLSNShmemSize());
 	size = add_size(size, LogicalDecodingCtlShmemSize());
+	size = add_size(size, DWBufShmemSize());
 
 	/* include additional requested shmem from preload libraries */
 	size = add_size(size, total_addin_request);
@@ -274,6 +276,7 @@ CreateOrAttachShmemStructs(void)
 	SUBTRANSShmemInit();
 	MultiXactShmemInit();
 	BufferManagerShmemInit();
+	DWBufShmemInit();
 
 	/*
 	 * Set up lock manager
-- 
2.43.0

From 59e27587ff6a6c57586256ef38d91140997a3e0c Mon Sep 17 00:00:00 2001
From: baotiao <[email-masked]>
Date: Sun, 8 Feb 2026 07:22:31 +0800
Subject: [PATCH v1 4/4] Fix critical correctness bugs in double write buffer
 (DWB)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fix several issues that made the DWB feature unsafe:

1. CRC computation: Move crc to first field of DWBufPageSlot and zero
   it before computing, so the stale crc value is excluded from the
   CRC range. Previously verification would always fail.

2. Fsync before data file writes: DWB pages must be durable before
   data file writes begin. Add per-page DWBufFlushFile() for
   non-checkpoint writes (bgwriter/backend eviction), and batch
   DWBufFlush() in BufferSync for checkpoint writes.

3. Checkpoint integration: Wire up DWBufPreCheckpoint/PostCheckpoint
   calls in CreateCheckPoint and CreateRestartPoint — they existed
   but were never called.

4. Recovery path: Wire up DWBufRecoveryInit in StartupXLOG to build
   recovery hash, DWBufRecoverPage in buffer_readv_complete to
   recover torn pages, and DWBufRecoveryFinish after FinishWalRecovery.

5. Backup FPW: Keep full_page_writes enabled when a backup is running
   even with DWB on, since pg_basebackup doesn't copy DWB files.

6. Slot overflow: Add bounds check when write_pos >= num_slots,
   flush and wrap instead of overwriting valid data.

7. PostCheckpoint race: Add resetting flag to prevent concurrent
   DWBufWritePage calls during position reset.

8. Build/initdb: Add dwbuf.c to meson.build, pg_dwbuf to initdb
   subdirs array.

9. Minor: Remove unused old_batch_id, fix %ld format string cast.

Co-Authored-By: Claude Opus 4.6 <[email-masked]>
---
 src/backend/access/transam/xlog.c      |  28 +++++-
 src/backend/storage/buffer/bufmgr.c    | 106 ++++++++++++++++++---
 src/backend/storage/buffer/dwbuf.c     | 124 ++++++++++++++++++-------
 src/backend/storage/buffer/meson.build |   1 +
 src/bin/initdb/initdb.c                |   3 +-
 src/include/storage/dwbuf.h            |  14 ++-
 6 files changed, 223 insertions(+), 53 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 74808d2fcf..45deb700d9 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -847,9 +847,12 @@ XLogInsertRecord(XLogRecData *rdata,
 			RedoRecPtr = Insert->RedoRecPtr;
 		}
 		/*
-		 * If DWB is enabled, we don't need full page writes.
+		 * If DWB is enabled and no backup is running, we don't need full
+		 * page writes — DWB provides torn page protection.  But during
+		 * backups, FPW must stay on because pg_basebackup doesn't copy
+		 * DWB files, so the standby has no DWB to recover from.
 		 */
-		if (DWBufIsEnabled())
+		if (DWBufIsEnabled() && Insert->runningBackups == 0)
 			doPageWrites = false;
 		else
 			doPageWrites = (Insert->fullPageWrites || Insert->runningBackups > 0);
@@ -5876,6 +5879,9 @@ StartupXLOG(void)
 		 */
 		ResetUnloggedRelations(UNLOGGED_RELATION_CLEANUP);
 
+		/* Scan DWB files and build recovery hash for torn page recovery */
+		DWBufRecoveryInit();
+
 		/*
 		 * Likewise, delete any saved transaction snapshot files that got left
 		 * behind by crashed backends.
@@ -5962,6 +5968,7 @@ StartupXLOG(void)
 	 * Finish WAL recovery.
 	 */
 	endOfRecoveryInfo = FinishWalRecovery();
+	DWBufRecoveryFinish();		/* clean up DWB recovery hash */
 	EndOfLog = endOfRecoveryInfo->endOfLog;
 	EndOfLogTLI = endOfRecoveryInfo->endOfLogTLI;
 	abortedRecPtr = endOfRecoveryInfo->abortedRecPtr;
@@ -6601,8 +6608,9 @@ GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p)
 {
 	*RedoRecPtr_p = RedoRecPtr;
 	/*
-	 * If double write buffer is enabled, we don't need full page writes
-	 * because DWB provides torn page protection.
+	 * If DWB is enabled, hint that FPW is not needed.  This is only a
+	 * hint — XLogInsertRecord re-checks with the authoritative backup
+	 * state and will re-enable FPW if a backup is running.
 	 */
 	if (DWBufIsEnabled())
 		*doPageWrites_p = false;
@@ -7324,6 +7332,9 @@ CreateCheckPoint(int flags)
 	}
 	pfree(vxids);
 
+	/* Flush all pending DWB writes before checkpoint */
+	DWBufPreCheckpoint();
+
 	CheckPointGuts(checkPoint.redo, flags);
 
 	vxids = GetVirtualXIDsDelayingChkpt(&nvxids, DELAY_CHKPT_COMPLETE);
@@ -7446,6 +7457,9 @@ CreateCheckPoint(int flags)
 	 */
 	SyncPostCheckpoint();
 
+	/* Reset DWB for next checkpoint cycle */
+	DWBufPostCheckpoint(recptr);
+
 	/*
 	 * Update the average distance between checkpoints if the prior checkpoint
 	 * exists.
@@ -7832,6 +7846,9 @@ CreateRestartPoint(int flags)
 	/* Update the process title */
 	update_checkpoint_display(flags, true, false);
 
+	/* Flush all pending DWB writes before checkpoint */
+	DWBufPreCheckpoint();
+
 	CheckPointGuts(lastCheckPoint.redo, flags);
 
 	/*
@@ -7894,6 +7911,9 @@ CreateRestartPoint(int flags)
 	}
 	LWLockRelease(ControlFileLock);
 
+	/* Reset DWB for next checkpoint cycle */
+	DWBufPostCheckpoint(lastCheckPoint.redo);
+
 	/*
 	 * Update the average distance between checkpoints/restartpoints if the
 	 * prior checkpoint exists.
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 8c1e78fba2..597db1d521 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -3633,10 +3633,56 @@ BufferSync(int flags)
 	binaryheap_build(ts_heap);
 
 	/*
-	 * Iterate through to-be-checkpointed buffers and write the ones (still)
-	 * marked with BM_CHECKPOINT_NEEDED. The writes are balanced between
-	 * tablespaces; otherwise the sorting would lead to only one tablespace
-	 * receiving writes at a time, making inefficient use of the hardware.
+	 * Phase 1 (DWB): If double write buffer is enabled, write all
+	 * checkpoint-dirty pages to DWB first, then batch fsync once.
+	 * This ensures torn page protection before data file writes begin.
+	 */
+	if (DWBufIsEnabled())
+	{
+		char	   *dwb_buf;
+
+		dwb_buf = palloc_aligned(BLCKSZ, PG_IO_ALIGN_SIZE, 0);
+
+		for (i = 0; i < num_to_scan; i++)
+		{
+			BufferDesc *bufHdr;
+			uint64		state;
+			Page		page;
+
+			buf_id = CkptBufferIds[i].buf_id;
+			bufHdr = GetBufferDescriptor(buf_id);
+			state = pg_atomic_read_u64(&bufHdr->state);
+
+			if (!(state & BM_CHECKPOINT_NEEDED))
+				continue;
+			if (state & BM_IO_IN_PROGRESS)
+				continue;		/* being written by someone else */
+
+			/* Copy page content — buffer is readable with shared lock */
+			page = BufHdrGetBlock(bufHdr);
+			memcpy(dwb_buf, page, BLCKSZ);
+
+			DWBufWritePage(BufTagGetRelFileLocator(&bufHdr->tag),
+						   BufTagGetForkNum(&bufHdr->tag),
+						   bufHdr->tag.blockNum,
+						   dwb_buf,
+						   BufferGetLSN(bufHdr));
+		}
+
+		/* Single batch fsync for all DWB writes */
+		DWBufFlush();
+		pfree(dwb_buf);
+
+		/* Tell FlushBuffer to skip per-page DWB writes */
+		DWBufSetCheckpointWritesDone(true);
+	}
+
+	/*
+	 * Phase 2: Iterate through to-be-checkpointed buffers and write the
+	 * ones (still) marked with BM_CHECKPOINT_NEEDED. The writes are
+	 * balanced between tablespaces; otherwise the sorting would lead to
+	 * only one tablespace receiving writes at a time, making inefficient
+	 * use of the hardware.
 	 */
 	num_processed = 0;
 	num_written = 0;
@@ -3708,6 +3754,10 @@ BufferSync(int flags)
 	 */
 	IssuePendingWritebacks(&wb_context, IOCONTEXT_NORMAL);
 
+	/* Clear the checkpoint-writes-done flag */
+	if (DWBufIsEnabled())
+		DWBufSetCheckpointWritesDone(false);
+
 	pfree(per_ts_stat);
 	per_ts_stat = NULL;
 	binaryheap_free(ts_heap);
@@ -4499,17 +4549,21 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object,
 	io_start = pgstat_prepare_io_time(track_io_timing);
 
 	/*
-	 * If double write buffer is enabled, write the page to DWB first.
-	 * This protects against torn pages without needing full page writes in WAL.
-	 * DWBufWritePage now includes fsync internally for correctness.
+	 * If double write buffer is enabled and checkpoint has not already
+	 * written this page to DWB, write it now with per-page fsync.
+	 * This protects against torn pages without needing full page writes
+	 * in WAL.
 	 */
-	if (DWBufIsEnabled())
+	if (DWBufIsEnabled() && !DWBufCheckpointWritesDone())
 	{
-		DWBufWritePage(BufTagGetRelFileLocator(&buf->tag),
-					   BufTagGetForkNum(&buf->tag),
-					   buf->tag.blockNum,
-					   bufToWrite,
-					   recptr);
+		int			dwb_file_idx;
+
+		dwb_file_idx = DWBufWritePage(BufTagGetRelFileLocator(&buf->tag),
+									  BufTagGetForkNum(&buf->tag),
+									  buf->tag.blockNum,
+									  bufToWrite,
+									  recptr);
+		DWBufFlushFile(dwb_file_idx);
 	}
 
 	/*
@@ -8190,6 +8244,30 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
 		if (!PageIsVerified((Page) bufdata, tag.blockNum, piv_flags,
 							failed_checksum))
 		{
+			/*
+			 * Page verification failed — try to recover from the
+			 * double write buffer before giving up.
+			 */
+			if (DWBufRecoverPage(BufTagGetRelFileLocator(&tag),
+								 BufTagGetForkNum(&tag),
+								 tag.blockNum,
+								 (char *) bufdata))
+			{
+				/* Re-verify the recovered page */
+				bool	recovered_checksum_failure = false;
+
+				if (PageIsVerified((Page) bufdata, tag.blockNum,
+								   piv_flags, &recovered_checksum_failure))
+				{
+					/* Successfully recovered from DWB */
+					elog(LOG, "recovered torn page %u/%u/%u fork %d block %u from double write buffer",
+						 tag.spcOid, BufTagGetRelFileLocator(&tag).dbOid,
+						 BufTagGetRelFileLocator(&tag).relNumber,
+						 BufTagGetForkNum(&tag), tag.blockNum);
+					goto page_verified;
+				}
+			}
+
 			if (flags & READ_BUFFERS_ZERO_ON_ERROR)
 			{
 				memset(bufdata, 0, BLCKSZ);
@@ -8205,6 +8283,8 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
 		else if (*failed_checksum)
 			*ignored_checksum = true;
 
+page_verified:
+
 		/* undo what we did above */
 #ifdef USE_VALGRIND
 		if (!BufferIsPinned(buffer))
diff --git a/src/backend/storage/buffer/dwbuf.c b/src/backend/storage/buffer/dwbuf.c
index 5c5b14f5b3..9943308323 100644
--- a/src/backend/storage/buffer/dwbuf.c
+++ b/src/backend/storage/buffer/dwbuf.c
@@ -140,6 +140,7 @@ DWBufShmemInit(void)
 		DWBufCtl->batch_id = 0;
 		DWBufCtl->flushed_batch_id = 0;
 		DWBufCtl->checkpoint_lsn = InvalidXLogRecPtr;
+		DWBufCtl->resetting = false;
 	}
 }
 
@@ -306,12 +307,14 @@ DWBufClose(void)
 }
 
 /*
- * Write a page to the double write buffer and fsync.
+ * Write a page to the double write buffer.
  *
- * This function writes the page to DWB and ensures it's fsynced to disk
- * before returning, guaranteeing torn page protection.
+ * Returns the file index that was written to, so the caller can fsync
+ * that specific file if needed (e.g. for non-checkpoint writes).
+ *
+ * Returns -1 if DWB is not enabled.
  */
-void
+int
 DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
 			   BlockNumber blkno, const char *page, XLogRecPtr lsn)
 {
@@ -323,7 +326,11 @@ DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
 	pg_crc32c	crc;
 
 	if (!double_write_buffer || DWBufCtl == NULL)
-		return;
+		return -1;
+
+	/* Wait if DWB is being reset by PostCheckpoint */
+	while (DWBufCtl->resetting)
+		pg_usleep(100);
 
 	/* Ensure files are opened in this process */
 	DWBufOpenFiles();
@@ -331,15 +338,23 @@ DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
 	/* Get next slot position atomically */
 	pos = pg_atomic_fetch_add_u64(&DWBufCtl->write_pos, 1);
 
+	/* If DWB is full, flush before overwriting */
+	if (pos >= (uint64) DWBufCtl->num_slots)
+	{
+		DWBufFlush();
+		pos = pos % DWBufCtl->num_slots;
+	}
+
 	/* Calculate file and slot indices */
-	file_idx = (pos / DWBufCtl->slots_per_file) % DWBufCtl->num_files;
-	slot_idx = pos % DWBufCtl->slots_per_file;
+	file_idx = (pos % DWBufCtl->num_slots) / DWBufCtl->slots_per_file;
+	slot_idx = (pos % DWBufCtl->num_slots) % DWBufCtl->slots_per_file;
 
 	/* Calculate offset in file */
 	offset = sizeof(DWBufFileHeader) + (off_t) slot_idx * DWBUF_SLOT_SIZE;
 
 	/* Build slot header in local buffer */
 	slot = (DWBufPageSlot *) dwbuf_page_buffer;
+	slot->crc = 0;				/* zero before CRC computation */
 	slot->rlocator = rlocator;
 	slot->forknum = forknum;
 	slot->blkno = blkno;
@@ -351,7 +366,7 @@ DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
 	/* Copy page data after header */
 	memcpy(dwbuf_page_buffer + sizeof(DWBufPageSlot), page, BLCKSZ);
 
-	/* Compute CRC over slot header and page data */
+	/* Compute CRC over slot header (excluding crc field) and page data */
 	INIT_CRC32C(crc);
 	COMP_CRC32C(crc, dwbuf_page_buffer + sizeof(pg_crc32c),
 				sizeof(DWBufPageSlot) - sizeof(pg_crc32c) + BLCKSZ);
@@ -365,10 +380,7 @@ DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
 				(errcode_for_file_access(),
 				 errmsg("could not write to double write buffer: %m")));
 
-	/*
-	 * NOTE: We don't fsync immediately here for performance reasons.
-	 * The DWBufFlush() function will fsync all files before checkpoint.
-	 */
+	return file_idx;
 }
 
 /*
@@ -411,6 +423,30 @@ DWBufFlush(void)
 	pg_atomic_write_u64(&DWBufCtl->flush_pos, current_pos);
 }
 
+/*
+ * Fsync a single DWB file by index.
+ * Used for per-page fsync in the non-checkpoint write path.
+ */
+void
+DWBufFlushFile(int file_idx)
+{
+	if (!double_write_buffer || DWBufCtl == NULL)
+		return;
+
+	/* Ensure files are opened in this process */
+	if (DWBufFilesOpenedPid != getpid() || DWBufFds[0] < 0)
+		DWBufOpenFiles();
+
+	if (file_idx >= 0 && file_idx < DWBufCtl->num_files && DWBufFds[file_idx] >= 0)
+	{
+		if (pg_fsync(DWBufFds[file_idx]) != 0)
+			ereport(ERROR,
+					(errcode_for_file_access(),
+					 errmsg("could not fsync double write buffer file %d: %m",
+							file_idx)));
+	}
+}
+
 /*
  * Flush all pages and ensure DWB is fully synced.
  */
@@ -447,7 +483,6 @@ void
 DWBufPostCheckpoint(XLogRecPtr checkpoint_lsn)
 {
 	int			i;
-	uint64		old_batch_id;
 	uint64		new_batch_id;
 
 	if (!double_write_buffer || DWBufCtl == NULL)
@@ -457,33 +492,34 @@ DWBufPostCheckpoint(XLogRecPtr checkpoint_lsn)
 	if (DWBufFilesOpenedPid != getpid() || DWBufFds[0] < 0)
 		DWBufOpenFiles();
 
+	/*
+	 * Signal writers to wait — prevents new DWBufWritePage calls from
+	 * racing with our reset of write_pos.
+	 */
 	SpinLockAcquire(&DWBufCtl->mutex);
-
-	/* Save old batch ID and increment */
-	old_batch_id = DWBufCtl->batch_id;
-	DWBufCtl->batch_id++;
-	new_batch_id = DWBufCtl->batch_id;
-	DWBufCtl->checkpoint_lsn = checkpoint_lsn;
-
+	DWBufCtl->resetting = true;
 	SpinLockRelease(&DWBufCtl->mutex);
 
 	/*
-	 * Wait for all in-flight writes to complete before resetting write_pos.
-	 * We use batch_id as a synchronization point.
+	 * Wait briefly for any in-flight DWBufWritePage calls to finish their
+	 * pg_pwrite.  They have already obtained their slot position, so we
+	 * just need them to complete the write before we reset positions.
 	 */
-	{
-		uint64 current_pos = pg_atomic_read_u64(&DWBufCtl->write_pos);
-		uint64 num_slots = DWBufCtl->num_slots;
-
-		/* If write_pos wrapped around, wait for flush */
-		if (current_pos >= num_slots)
-			DWBufFlush();
-	}
+	pg_memory_barrier();
+	pg_usleep(1000);	/* 1ms — conservative */
 
 	/* Now safe to reset positions for new batch */
 	pg_atomic_write_u64(&DWBufCtl->write_pos, 0);
 	pg_atomic_write_u64(&DWBufCtl->flush_pos, 0);
 
+	/* Update batch and clear resetting flag */
+	SpinLockAcquire(&DWBufCtl->mutex);
+	DWBufCtl->batch_id++;
+	new_batch_id = DWBufCtl->batch_id;
+	DWBufCtl->checkpoint_lsn = checkpoint_lsn;
+	DWBufCtl->resetting = false;
+	SpinLockRelease(&DWBufCtl->mutex);
+
 	/* Update file headers with new batch info */
 	for (i = 0; i < DWBufCtl->num_files; i++)
 	{
@@ -667,7 +703,7 @@ DWBufRecoveryInit(void)
 	pfree(buffer);
 
 	elog(LOG, "double write buffer recovery initialized with %ld pages",
-		 hash_get_num_entries(dwbuf_recovery_hash));
+		 (long) hash_get_num_entries(dwbuf_recovery_hash));
 }
 
 /*
@@ -784,3 +820,29 @@ DWBufGetBatchId(void)
 
 	return batch_id;
 }
+
+/*
+ * Module-static flag: when true, FlushBuffer skips DWB writes because
+ * checkpoint Phase 1 has already written all pages to DWB and fsynced.
+ */
+static bool dwbuf_checkpoint_writes_done = false;
+
+/*
+ * Set the checkpoint-writes-done flag.
+ * Called by BufferSync to bracket the checkpoint write loop.
+ */
+void
+DWBufSetCheckpointWritesDone(bool done)
+{
+	dwbuf_checkpoint_writes_done = done;
+}
+
+/*
+ * Check if checkpoint DWB writes are already done.
+ * FlushBuffer uses this to skip per-page DWB writes during checkpoint.
+ */
+bool
+DWBufCheckpointWritesDone(void)
+{
+	return dwbuf_checkpoint_writes_done;
+}
diff --git a/src/backend/storage/buffer/meson.build b/src/backend/storage/buffer/meson.build
index ed84bf0897..dafdcb65d5 100644
--- a/src/backend/storage/buffer/meson.build
+++ b/src/backend/storage/buffer/meson.build
@@ -4,6 +4,7 @@ backend_sources += files(
   'buf_init.c',
   'buf_table.c',
   'bufmgr.c',
+  'dwbuf.c',
   'freelist.c',
   'localbuf.c',
 )
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index a3980e5535..4042241877 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -251,7 +251,8 @@ static const char *const subdirs[] = {
 	"pg_xact",
 	"pg_logical",
 	"pg_logical/snapshots",
-	"pg_logical/mappings"
+	"pg_logical/mappings",
+	"pg_dwbuf"
 };

diff --git a/src/include/storage/dwbuf.h b/src/include/storage/dwbuf.h
index 1b096867f2..bf20d691f6 100644
--- a/src/include/storage/dwbuf.h
+++ b/src/include/storage/dwbuf.h
@@ -34,11 +34,11 @@
  */
 typedef struct DWBufPageSlot
 {
+	pg_crc32c		crc;			/* CRC of slot header + page — MUST BE FIRST */
 	RelFileLocator	rlocator;		/* Relation file locator */
 	ForkNumber		forknum;		/* Fork number */
 	BlockNumber		blkno;			/* Block number in relation */
 	XLogRecPtr		lsn;			/* Page LSN at write time */
-	pg_crc32c		crc;			/* CRC of slot header + page content */
 	uint32			slot_id;		/* Slot identifier */
 	uint16			flags;			/* Slot flags */
 	uint16			checksum;		/* Page checksum (if enabled) */
@@ -85,6 +85,7 @@ typedef struct DWBufCtlData
 	uint64			batch_id;		/* Current batch ID */
 	uint64			flushed_batch_id;	/* Last fully flushed batch */
 	XLogRecPtr		checkpoint_lsn;	/* LSN of last checkpoint */
+	bool			resetting;		/* True during PostCheckpoint reset */
 
 	/* Configuration (set at startup) */
 	int				num_slots;		/* Total number of slots */
@@ -117,12 +118,17 @@ extern void DWBufInit(void);
 extern void DWBufClose(void);
 
 /* Write operations */
-extern void DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
-						   BlockNumber blkno, const char *page,
-						   XLogRecPtr lsn);
+extern int DWBufWritePage(RelFileLocator rlocator, ForkNumber forknum,
+						  BlockNumber blkno, const char *page,
+						  XLogRecPtr lsn);
+extern void DWBufFlushFile(int file_idx);
 extern void DWBufFlush(void);
 extern void DWBufFlushAll(void);
 
+/* Checkpoint batch write support */
+extern void DWBufSetCheckpointWritesDone(bool done);
+extern bool DWBufCheckpointWritesDone(void);
+
 /* Checkpoint integration */
 extern void DWBufPreCheckpoint(void);
 extern void DWBufPostCheckpoint(XLogRecPtr checkpoint_lsn);
-- 
2.43.0

---

From: Robert Treat <[email-masked]>
Date: Wed, 18 Feb 2026 13:00:00 -0500

> else is busy with work on other patches before commit freeze.

I'm somewhat less of a noob here, so I'll confirm that this proposal
has basically zero chance of getting in, at least for the v19 cycle.
This isn't so much about the proposal itself, but more in that if you
were trying to pick the worst time of year to submit a large,
complicated feature into the postgresql workflow, this would be really
close to that.

However, I have also wondered about this specific trade-off (FPW vs
DWB) for years, but until now, the level of effort required to produce
a meaningful POC that would confirm if the idea was worth pursuing was
so large that I think it stopped anyone from even trying. So,
hopefully everyone will realize that we don't live in that world
anymore, and as a side benefit, apparently the idea is worth pursuing.

> Some notes/questions about the patches itself:

So, I haven't looked at the code itself; tbh honest I am a bit too
paranoid to dive into generated code that would seem to carry some
likely level of legal risk around potential reuse of GPL/proprietary
code it might be based on (either in its original training, inference,
or context used for generation. Yeah, I know innodb isn't written in
C, but still). That said, I did have some feedback and questions on
the proposal itself, and some suggestions for how to move things
forward.

I would be helpful if you could provide a little more information on
the system you are running these benchmarks on, specifically for me
the underlying OS/Filesystem/hardware, and I'd even be interested in
the build flags. I'd also be interested to know if you did any kind of
crash safety testing... while it is great to have improved
performance, presumably that isn't actually the primary point of these
subsystems. It'd also be worth knowing if you tested this on any
systems with replication (physical or logical) since we'd need to
understand those potential downstream effects. I'm tempted to say you
should have an AI generate some pgbench scripts. Granted its early and
fine if you have done any of this, but I imagine we'll need to look at
it eventually.

>    BTW the numbers are quite impressive)

While Jakub is right that the convention is to send patches, that
convention is based on a manual development model, not an agentic
development model. While there is no official project policy on this,
IMHO the thing we really need from you is not the code output, but the
prompts that were used to generate the code. There are plenty of folks
who have access to claude that could then use those prompts to
"recreate with enough proximity" the work you had claude do, and that
process would also allow for additional verification and reduction of
any legal concerns or concerns about investing further human
time/energy. (No offense, but as you are not a regular contributor,
you could analogize this to when third parties do large code dumps and
say "here's a contribution, it's up to you to figure out how to use
it". Ideally we want other folks to be able to pick up the project and
continue with it, even if it means recreating it, and that works best
if we have the underlying prompts).

The claude code configuration file is a good start, but certainly not
enough. Probably the ideal here would be full session logs, although a
developer-diary would probably also suffice. I'm kind of guessing here
because I don't know the scope of the prompts involved or how you were
interacting with Claude in order to get where you are now, but those
seem like the more obvious tools for work of this size whose intention
is to be open.

Robert Treat
https://xzilla.net

---

From: wenhui qiu <[email-masked]>
Date: Thu, 19 Feb 2026 16:19:44 +0800

HI Robert
     In fact, this was discussed over a decade ago.（
https://www.postgresql.org/message-id/1962493974.656458.1327703514780.JavaMail.root%40zimbra-prod-mbox-4.vmware.com
）,In practice, this mainly stems from the significant overhead introduced
by FPW. The community has adopted various approaches to mitigate its
impact, including compressing FPW and extending checkpoint intervals.
Nowadays, waiting for the primary to recover instead of performing an HA
switchover is generally considered unacceptable.

Thanks