From 5712ea267634e666dff4e1b0e89ebd453ea2bd02 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 10 Jul 2026 14:26:13 +0300 Subject: [PATCH 01/52] Add short-lived double write buffer skeleton (Stage 1) Introduce the pg_dwb subsystem: an alternative torn-page protection to full-page writes, shaped after InnoDB's doublewrite buffer. A DWB slot lives for the duration of one flush batch and is reused only after the data-file fsync covering its page has become durable, per-batch order: XLogFlush -> batch write + fdatasync -> smgrwrite -> segment fsync. This stage brings the standalone skeleton, not yet wired into FlushBuffer: * storage/dwb module: the batch state machine (FREE -> ALLOCATED -> SEALED -> WRITTEN -> FSYNCED -> DATA_WRITTEN -> RETIRING -> FREE), slot reservation via a 31-bit index with a SEAL_BIT sentinel, seal/leader election, the leader batch write (contiguous image stream, then the meta region, then fdatasync) and abandoned-slot cleanup on process exit. Retirement is synchronous for now; the retire worker pool arrives with the FlushBuffer integration. * On-disk ring under $PGDATA/pg_dwb/: zero-preallocated batch files (fixed size, so fdatasync suffices for durability, the WAL-segment contract) and an atomically replaced control file carrying the geometry and a durable generation, bumped on every start before the ring opens. Slot validity is locally verifiable: meta_crc rejects a torn meta write (including old/new field mixes on slot reuse), image_crc rejects a torn image. * Shared-memory state with an IO-aligned staging pool: writers publish page images by memcpy only, all pg_dwb I/O is done by leaders. * GUCs (io_torn_pages_protection plus the dwb_* family), enforcement of data checksums in double_writes mode, wait events and LWLocks. * test_dwb module: pg_regress tests for write cycles and on-disk CRC validation, TAP coverage for concurrent writers over a small ring, the generation bump across restarts, geometry mismatch and the checksum requirement. --- src/backend/access/transam/xlog.c | 9 + src/backend/storage/Makefile | 2 +- src/backend/storage/dwb/Makefile | 21 + src/backend/storage/dwb/dwb.c | 599 ++++++++++++++++++ src/backend/storage/dwb/dwb_ctl.c | 112 ++++ src/backend/storage/dwb/dwb_file.c | 335 ++++++++++ src/backend/storage/dwb/dwb_recovery.c | 81 +++ src/backend/storage/dwb/meson.build | 8 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/lmgr/lwlock.c | 1 + src/backend/storage/meson.build | 1 + .../utils/activity/wait_event_names.txt | 11 + src/backend/utils/misc/guc_tables.c | 132 ++++ src/backend/utils/misc/postgresql.conf.sample | 17 + src/include/storage/dwb.h | 262 ++++++++ src/include/storage/lwlock.h | 1 + src/include/storage/lwlocklist.h | 1 + src/test/modules/Makefile | 1 + src/test/modules/meson.build | 1 + src/test/modules/test_dwb/.gitignore | 4 + src/test/modules/test_dwb/Makefile | 29 + .../modules/test_dwb/expected/test_dwb.out | 38 ++ src/test/modules/test_dwb/meson.build | 40 ++ src/test/modules/test_dwb/sql/test_dwb.sql | 18 + src/test/modules/test_dwb/t/001_dwb.pl | 83 +++ src/test/modules/test_dwb/test_dwb--1.0.sql | 20 + src/test/modules/test_dwb/test_dwb.c | 237 +++++++ src/test/modules/test_dwb/test_dwb.conf | 3 + src/test/modules/test_dwb/test_dwb.control | 4 + 29 files changed, 2073 insertions(+), 1 deletion(-) create mode 100644 src/backend/storage/dwb/Makefile create mode 100644 src/backend/storage/dwb/dwb.c create mode 100644 src/backend/storage/dwb/dwb_ctl.c create mode 100644 src/backend/storage/dwb/dwb_file.c create mode 100644 src/backend/storage/dwb/dwb_recovery.c create mode 100644 src/backend/storage/dwb/meson.build create mode 100644 src/include/storage/dwb.h create mode 100644 src/test/modules/test_dwb/.gitignore create mode 100644 src/test/modules/test_dwb/Makefile create mode 100644 src/test/modules/test_dwb/expected/test_dwb.out create mode 100644 src/test/modules/test_dwb/meson.build create mode 100644 src/test/modules/test_dwb/sql/test_dwb.sql create mode 100644 src/test/modules/test_dwb/t/001_dwb.pl create mode 100644 src/test/modules/test_dwb/test_dwb--1.0.sql create mode 100644 src/test/modules/test_dwb/test_dwb.c create mode 100644 src/test/modules/test_dwb/test_dwb.conf create mode 100644 src/test/modules/test_dwb/test_dwb.control diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index e07cb9103515f..f681164ea28ca 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -83,6 +83,7 @@ #include "replication/walreceiver.h" #include "replication/walsender.h" #include "storage/bufmgr.h" +#include "storage/dwb.h" #include "storage/fd.h" #include "storage/ipc.h" #include "storage/large_object.h" @@ -5594,6 +5595,14 @@ StartupXLOG(void) else didCrash = false; + /* + * Create or validate the double write buffer ring and durably bump its + * generation before any of its slots can be written or applied. The + * apply-pass over the previous generation runs here, before WAL + * recovery is initialized. + */ + DWBStartup(); + /* * Prepare for WAL recovery if needed. * diff --git a/src/backend/storage/Makefile b/src/backend/storage/Makefile index eec03f6f2b4c5..46c960f2248a6 100644 --- a/src/backend/storage/Makefile +++ b/src/backend/storage/Makefile @@ -8,6 +8,6 @@ subdir = src/backend/storage top_builddir = ../../.. include $(top_builddir)/src/Makefile.global -SUBDIRS = aio buffer file freespace ipc large_object lmgr page smgr sync +SUBDIRS = aio buffer dwb file freespace ipc large_object lmgr page smgr sync include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/storage/dwb/Makefile b/src/backend/storage/dwb/Makefile new file mode 100644 index 0000000000000..c45c8b16442a8 --- /dev/null +++ b/src/backend/storage/dwb/Makefile @@ -0,0 +1,21 @@ +#------------------------------------------------------------------------- +# +# Makefile-- +# Makefile for storage/dwb +# +# IDENTIFICATION +# src/backend/storage/dwb/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/backend/storage/dwb +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global + +OBJS = \ + dwb.o \ + dwb_ctl.o \ + dwb_file.o \ + dwb_recovery.o + +include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c new file mode 100644 index 0000000000000..b94e5a8a1a5de --- /dev/null +++ b/src/backend/storage/dwb/dwb.c @@ -0,0 +1,599 @@ +/*------------------------------------------------------------------------- + * + * dwb.c + * Batch state machine of the short-lived double write buffer. + * + * Batch lifecycle: FREE -> ALLOCATED -> SEALED -> WRITTEN -> FSYNCED -> + * DATA_WRITTEN -> RETIRING -> FREE. Writers reserve slots with an atomic + * fetch_add on next_slot_idx (31-bit index + SEAL_BIT sentinel), publish + * their page image with a plain memcpy into the batch's staging buffer and + * set their bit in slots_written_bitmap. The SEAL initiator becomes the + * leader: it waits for bitmap coverage of capped_slots, then writes the + * whole batch (contiguous image stream, then the meta region, then + * fdatasync) and broadcasts DWB_FSYNCED. + * + * Stage 1 scope: the state machine is complete but not yet wired into + * FlushBuffer; retirement is synchronous (DWBRetireAllSync) — the retire + * worker pool and the segment back-reference hash arrive in Stage 2. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/storage/dwb/dwb.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "miscadmin.h" +#include "port/pg_bitutils.h" +#include "storage/dwb.h" +#include "storage/ipc.h" +#include "storage/proc.h" +#include "utils/memutils.h" +#include "utils/timestamp.h" +#include "utils/wait_event.h" + +/* + * Slot refs held by this backend, for cleanup on process exit. A ref lives + * from DWBAcquireSlot to DWBReleaseSlot. (Stage 2 additionally attaches + * refs to the ResourceOwner so that a transaction abort — e.g. an ERROR out + * of smgrwrite — releases them too.) + */ +static DWBSlotRef pendingRefs[2 * DWB_BATCH_MAX_PAGES]; +static int nPendingRefs = 0; +static bool cleanup_registered = false; + +static void DWBProcExit(int code, Datum arg); +static void DWBLeaderWriteBatch(int batch_idx); +static void DWBFinishBatchData(DWBatchCtl *batch); + +static inline char * +DWBStagingSlotPtr(int staging_idx, int slot_idx) +{ + return DWBStagingBase + + (Size) staging_idx * dwb_batch_pages * BLCKSZ + + (Size) slot_idx * BLCKSZ; +} + +/* ---------------------------------------------------------------- + * staging pool + * ---------------------------------------------------------------- + */ +static int +DWBStagingAlloc(void) +{ + for (;;) + { + int idx = -1; + + SpinLockAcquire(&DWBCtl->staging_lock); + if (DWBCtl->staging_free != 0) + { + idx = pg_rightmost_one_pos32(DWBCtl->staging_free); + DWBCtl->staging_free &= ~(1U << idx); + } + SpinLockRelease(&DWBCtl->staging_lock); + + if (idx >= 0) + return idx; + + /* released together with retired batches / after leader writes */ + ConditionVariableSleep(&DWBCtl->cv_free_batch, + WAIT_EVENT_DWB_FREE_BATCH); + } +} + +static void +DWBStagingRelease(int idx) +{ + SpinLockAcquire(&DWBCtl->staging_lock); + DWBCtl->staging_free |= 1U << idx; + SpinLockRelease(&DWBCtl->staging_lock); + ConditionVariableBroadcast(&DWBCtl->cv_free_batch); +} + +/* ---------------------------------------------------------------- + * batch opening + * ---------------------------------------------------------------- + */ + +/* + * Make open_batch_idx[wclass] point at an ALLOCATED batch, if it currently + * points at old_idx (a sealed or invalid batch). Serialized by + * DWBRingOpenLock; sleeps on cv_free_batch when the whole ring is busy. + * + * Ordering note for stale writers: a batch keeps SEAL_BIT in next_slot_idx + * from its SEAL until we finish re-initializing it here, so a stale + * fetch_add against a reused batch either sees SEAL_BIT (and retries) or + * lands on a valid slot of the new incarnation — never on a slot that a + * concurrent reset can wipe. + */ +static void +DWBOpenNewBatch(int wclass, uint32 old_idx) +{ + for (;;) + { + int free_idx = -1; + + LWLockAcquire(DWBRingOpenLock, LW_EXCLUSIVE); + + /* someone else already replaced the open batch: done */ + if (pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]) != old_idx) + { + LWLockRelease(DWBRingOpenLock); + return; + } + + for (int i = 0; i < dwb_num_batches; i++) + { + uint32 expected = DWB_FREE; + + if (pg_atomic_compare_exchange_u32(&DWBCtl->batches[i].state, + &expected, DWB_ALLOCATED)) + { + free_idx = i; + break; + } + } + + if (free_idx >= 0) + { + DWBatchCtl *batch = &DWBCtl->batches[free_idx]; + + for (int w = 0; w < DWB_BITMAP_WORDS; w++) + pg_atomic_write_u64(&batch->slots_written_bitmap[w], 0); + pg_atomic_write_u32(&batch->capped_slots, 0); + pg_atomic_write_u32(&batch->ref_count, 0); + pg_atomic_write_u32(&batch->seg_pending_count, 0); + pg_atomic_write_u32(&batch->orphaned_refs_count, 0); + batch->n_segs = 0; + batch->max_page_lsn = InvalidXLogRecPtr; + batch->batch_id = pg_atomic_fetch_add_u64(&DWBCtl->next_batch_id, 1); + batch->open_time = GetCurrentTimestamp(); + batch->staging_idx = DWBStagingAlloc(); + + /* + * Open for reservations only after everything above is visible: + * clearing SEAL_BIT is the point where writers may enter. + */ + pg_write_barrier(); + pg_atomic_write_u32(&batch->next_slot_idx, 0); + + pg_atomic_write_u32(&DWBCtl->open_batch_idx[wclass], free_idx); + LWLockRelease(DWBRingOpenLock); + return; + } + + LWLockRelease(DWBRingOpenLock); + + /* whole ring busy: wait for a retirement, then retry */ + ConditionVariableSleep(&DWBCtl->cv_free_batch, + WAIT_EVENT_DWB_FREE_BATCH); + } +} + +/* ---------------------------------------------------------------- + * sealing and the leader write + * ---------------------------------------------------------------- + */ + +/* + * Seal a batch. Returns true if we won the seal race and performed the + * leader duties (the batch is DWB_FSYNCED — or fully cascaded to FREE for + * the defensive capped_slots == 0 case — on return). + */ +static bool +DWBSealBatch(int batch_idx) +{ + DWBatchCtl *batch = &DWBCtl->batches[batch_idx]; + uint32 prev; + uint32 capped; + uint32 expected; + + prev = pg_atomic_fetch_or_u32(&batch->next_slot_idx, DWB_SEAL_BIT); + if (prev & DWB_SEAL_BIT) + return false; /* somebody else is the leader */ + + capped = Min(prev & DWB_IDX_MASK, (uint32) dwb_batch_pages); + pg_atomic_write_u32(&batch->capped_slots, capped); + pg_write_barrier(); + + expected = DWB_ALLOCATED; + if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, DWB_SEALED)) + elog(PANIC, "DWB batch %d sealed in unexpected state %u", + batch_idx, expected); + ConditionVariableBroadcast(&batch->cv_state); + + if (capped == 0) + { + /* + * Defensive: a seal without a single reservation. No leader write; + * cascade to FREE without I/O or publication. + */ + DWBStagingRelease(batch->staging_idx); + batch->staging_idx = -1; + pg_atomic_write_u32(&batch->state, DWB_FREE); + ConditionVariableBroadcast(&DWBCtl->cv_free_batch); + return true; + } + + DWBLeaderWriteBatch(batch_idx); + return true; +} + +/* + * Leader: wait for bitmap coverage of capped_slots, write the batch, + * fdatasync, broadcast DWB_FSYNCED. + */ +static void +DWBLeaderWriteBatch(int batch_idx) +{ + DWBatchCtl *batch = &DWBCtl->batches[batch_idx]; + uint32 capped = pg_atomic_read_u32(&batch->capped_slots); + static DWSlotMeta *metas = NULL; + DWBBatchHeader hdr; + TimestampTz wait_start = GetCurrentTimestamp(); + uint32 expected; + + /* + * Coverage wait is memcpy-bound: writers do no I/O between reserving a + * slot and setting their bit. The timeout is a defensive backstop + * (e.g. a writer stopped in a debugger); dead writers are covered by + * ref cleanup marking their slots DWB_SLOT_ABORTED. + */ + ConditionVariablePrepareToSleep(&batch->cv_state); + for (;;) + { + bool covered = true; + uint32 full_words = capped / 64; + uint32 tail_bits = capped % 64; + + for (uint32 i = 0; covered && i < full_words; i++) + if (pg_atomic_read_u64(&batch->slots_written_bitmap[i]) != + PG_UINT64_MAX) + covered = false; + if (covered && tail_bits > 0) + { + uint64 mask = (UINT64CONST(1) << tail_bits) - 1; + + if ((pg_atomic_read_u64(&batch->slots_written_bitmap[full_words]) & + mask) != mask) + covered = false; + } + if (covered) + break; + + if (ConditionVariableTimedSleep(&batch->cv_state, + dwb_slot_stuck_timeout_ms, + WAIT_EVENT_DWB_BATCH_COVERAGE) && + TimestampDifferenceExceeds(wait_start, GetCurrentTimestamp(), + dwb_slot_stuck_timeout_ms)) + elog(PANIC, "DWB batch %d coverage wait exceeded %d ms", + batch_idx, dwb_slot_stuck_timeout_ms); + } + ConditionVariableCancelSleep(); + + expected = DWB_SEALED; + if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, DWB_WRITTEN)) + elog(PANIC, "DWB batch %d written in unexpected state %u", + batch_idx, expected); + + /* assemble slot metas entirely from shmem arrays */ + if (metas == NULL) + metas = MemoryContextAllocZero(TopMemoryContext, + DWB_BATCH_MAX_PAGES * sizeof(DWSlotMeta)); + memset(metas, 0, capped * sizeof(DWSlotMeta)); + batch->max_page_lsn = InvalidXLogRecPtr; + for (uint32 i = 0; i < capped; i++) + { + DWSlotMeta *meta = &metas[i]; + + meta->tag = batch->pages[i]; + meta->page_lsn = batch->page_lsns[i]; + meta->generation = DWBCtl->ring_generation; + meta->flags = batch->slot_flags[i]; + meta->image_crc = batch->image_crcs[i]; + meta->meta_crc = DWBSlotMetaCrc(meta); + if (batch->page_lsns[i] > batch->max_page_lsn) + batch->max_page_lsn = batch->page_lsns[i]; + } + + memset(&hdr, 0, sizeof(hdr)); + hdr.magic = DWB_BATCH_MAGIC; + hdr.version = DWB_VERSION; + hdr.batch_id = batch->batch_id; + hdr.n_slots = capped; + hdr.crc = DWBBatchHeaderCrc(&hdr); + + DWBWriteBatch(batch_idx, &hdr, metas, + DWBStagingSlotPtr(batch->staging_idx, 0)); + + /* image pwrite done — staging can serve the next batch */ + DWBStagingRelease(batch->staging_idx); + batch->staging_idx = -1; + + expected = DWB_WRITTEN; + if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, DWB_FSYNCED)) + elog(PANIC, "DWB batch %d fsynced in unexpected state %u", + batch_idx, expected); + ConditionVariableBroadcast(&batch->cv_state); +} + +/* ---------------------------------------------------------------- + * writer API + * ---------------------------------------------------------------- + */ + +/* + * Reserve a slot in the open batch (Stage 1: single writer class), record + * the page tag and the segment ref, and take a batch ref. + */ +void +DWBAcquireSlot(const BufferTag *tag, DWBSlotRef *ref) +{ + const int wclass = DWB_WCLASS_EVICTION; + + Assert(DWBIsEnabled()); + Assert(nPendingRefs < 2 * DWB_BATCH_MAX_PAGES); + + if (!cleanup_registered) + { + on_proc_exit(DWBProcExit, 0); + cleanup_registered = true; + } + + for (;;) + { + uint32 idx = pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]); + DWBatchCtl *batch; + uint32 prev; + uint32 slot; + + if (idx == DWB_INVALID_BATCH) + { + DWBOpenNewBatch(wclass, idx); + continue; + } + + batch = &DWBCtl->batches[idx]; + prev = pg_atomic_fetch_add_u32(&batch->next_slot_idx, 1); + + if (prev & DWB_SEAL_BIT) + { + /* already sealed; the extra increment is harmless (3.4) */ + DWBOpenNewBatch(wclass, idx); + continue; + } + + slot = prev & DWB_IDX_MASK; + if (slot >= (uint32) dwb_batch_pages) + { + /* overflow: this writer seals and (if it wins) leads */ + DWBSealBatch(idx); + DWBOpenNewBatch(wclass, idx); + continue; + } + + /* valid reservation */ + batch->pages[slot] = *tag; + batch->slot_flags[slot] = 0; + + { + DWSegRef seg; + bool found = false; + + seg.rlocator = BufTagGetRelFileLocator(tag); + seg.forknum = BufTagGetForkNum(tag); + seg.segno = tag->blockNum / RELSEG_SIZE; + + SpinLockAcquire(&batch->seg_lock); + for (uint32 i = 0; i < batch->n_segs; i++) + { + if (RelFileLocatorEquals(batch->seg_set[i].rlocator, seg.rlocator) && + batch->seg_set[i].forknum == seg.forknum && + batch->seg_set[i].segno == seg.segno) + { + found = true; + break; + } + } + if (!found) + batch->seg_set[batch->n_segs++] = seg; + SpinLockRelease(&batch->seg_lock); + } + + pg_atomic_fetch_add_u32(&batch->ref_count, 1); + + ref->batch_idx = (int) idx; + ref->slot_idx = (int) slot; + ref->batch_id = batch->batch_id; + pendingRefs[nPendingRefs++] = *ref; + return; + } +} + +/* + * Publish the page image: memcpy into the batch's staging slot, record + * LSN and image CRC in shmem, set our bitmap bit. + */ +void +DWBPublishImage(const DWBSlotRef *ref, const char *image, XLogRecPtr page_lsn) +{ + DWBatchCtl *batch = &DWBCtl->batches[ref->batch_idx]; + + memcpy(DWBStagingSlotPtr(batch->staging_idx, ref->slot_idx), + image, BLCKSZ); + batch->page_lsns[ref->slot_idx] = page_lsn; + batch->image_crcs[ref->slot_idx] = DWBImageCrc(image); + + pg_write_barrier(); + pg_atomic_fetch_or_u64(&batch->slots_written_bitmap[ref->slot_idx / 64], + UINT64CONST(1) << (ref->slot_idx % 64)); + ConditionVariableBroadcast(&batch->cv_state); +} + +/* + * Wait until the batch's DWB copy is durable. The caller holds a batch + * ref, so the batch cannot be retired or reused under us. + */ +void +DWBWaitBatchFsynced(const DWBSlotRef *ref) +{ + DWBatchCtl *batch = &DWBCtl->batches[ref->batch_idx]; + + ConditionVariablePrepareToSleep(&batch->cv_state); + while (pg_atomic_read_u32(&batch->state) < DWB_FSYNCED) + ConditionVariableSleep(&batch->cv_state, WAIT_EVENT_DWB_BATCH_FSYNC); + ConditionVariableCancelSleep(); +} + +/* + * Step 7 of the write path: the last ref publishes the segment set and + * moves the batch to RETIRING. (Stage 2 publishes into DWSegmentHash here; + * Stage 1 retirement is DWBRetireAllSync.) + */ +static void +DWBFinishBatchData(DWBatchCtl *batch) +{ + uint32 expected = DWB_FSYNCED; + + if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, + DWB_DATA_WRITTEN)) + elog(PANIC, "DWB batch data-written in unexpected state %u", expected); + + pg_atomic_write_u32(&batch->seg_pending_count, batch->n_segs); + pg_write_barrier(); + + expected = DWB_DATA_WRITTEN; + if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, + DWB_RETIRING)) + elog(PANIC, "DWB batch retiring in unexpected state %u", expected); + ConditionVariableSignal(&DWBCtl->cv_retire_wake); +} + +/* + * Drop our batch ref after the data-file write. The last ref finishes the + * batch (see DWBFinishBatchData). + */ +void +DWBReleaseSlot(const DWBSlotRef *ref) +{ + DWBatchCtl *batch = &DWBCtl->batches[ref->batch_idx]; + + for (int i = 0; i < nPendingRefs; i++) + { + if (pendingRefs[i].batch_idx == ref->batch_idx && + pendingRefs[i].slot_idx == ref->slot_idx) + { + pendingRefs[i] = pendingRefs[--nPendingRefs]; + break; + } + } + + if (pg_atomic_fetch_sub_u32(&batch->ref_count, 1) == 1) + DWBFinishBatchData(batch); +} + +/* + * Force-seal the currently open batch of a writer class (used by tests + * now; the retire worker's dwb_batch_timeout_ms path in Stage 2). + * Returns true if a batch was sealed by us. + */ +bool +DWBForceSealOpenBatch(int wclass) +{ + uint32 idx = pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]); + + if (idx == DWB_INVALID_BATCH) + return false; + if (pg_atomic_read_u32(&DWBCtl->batches[idx].next_slot_idx) == 0) + return false; /* empty batch: nothing to seal */ + return DWBSealBatch((int) idx); +} + +/* + * Synchronously retire every RETIRING batch. Stage 1: batch images are + * already durable in the ring and the test pages have no real relation + * segments to fsync, so retirement is pure state bookkeeping. Stage 2 + * replaces this with segment fsyncs by the retire worker pool and + * ProcessSyncRequests. + */ +int +DWBRetireAllSync(void) +{ + int retired = 0; + + for (int i = 0; i < dwb_num_batches; i++) + { + DWBatchCtl *batch = &DWBCtl->batches[i]; + uint32 expected = DWB_RETIRING; + + if (pg_atomic_read_u32(&batch->state) != DWB_RETIRING) + continue; + + /* Stage 2: smgrimmedsync of each seg_set entry goes here */ + + pg_atomic_write_u32(&batch->seg_pending_count, 0); + if (pg_atomic_compare_exchange_u32(&batch->state, &expected, + DWB_FREE)) + { + retired++; + ConditionVariableBroadcast(&DWBCtl->cv_free_batch); + } + } + return retired; +} + +DWBatchState +DWBGetBatchState(int batch_idx) +{ + return (DWBatchState) pg_atomic_read_u32(&DWBCtl->batches[batch_idx].state); +} + +/* ---------------------------------------------------------------- + * process exit cleanup + * ---------------------------------------------------------------- + */ + +/* + * Runs strictly on shmem DWB state: by the time on_proc_exit callbacks run, + * LWLockReleaseAll has already dropped any content locks (ipc.c) and the + * private page copy died with the process, so the staged copy in the batch + * is the authoritative source for our slots. + */ +static void +DWBProcExit(int code, Datum arg) +{ + while (nPendingRefs > 0) + { + DWBSlotRef ref = pendingRefs[--nPendingRefs]; + DWBatchCtl *batch = &DWBCtl->batches[ref.batch_idx]; + uint64 bit = UINT64CONST(1) << (ref.slot_idx % 64); + pg_atomic_uint64 *word = + &batch->slots_written_bitmap[ref.slot_idx / 64]; + + if (!(pg_atomic_read_u64(word) & bit)) + { + /* copy never published: poison the slot so the seal-waiter + * wakes up and recovery ignores it */ + batch->slot_flags[ref.slot_idx] |= DWB_SLOT_ABORTED; + pg_write_barrier(); + pg_atomic_fetch_or_u64(word, bit); + ConditionVariableBroadcast(&batch->cv_state); + } + else + { + /* copy published but smgrwrite may not have happened: hand the + * write over to the retire worker (Stage 2 completes orphans) */ + uint32 n = pg_atomic_fetch_add_u32(&batch->orphaned_refs_count, 1); + + batch->orphan_tags[n] = batch->pages[ref.slot_idx]; + } + + if (pg_atomic_fetch_sub_u32(&batch->ref_count, 1) == 1 && + pg_atomic_read_u32(&batch->state) == DWB_FSYNCED) + DWBFinishBatchData(batch); + } +} diff --git a/src/backend/storage/dwb/dwb_ctl.c b/src/backend/storage/dwb/dwb_ctl.c new file mode 100644 index 0000000000000..fc75657c1343e --- /dev/null +++ b/src/backend/storage/dwb/dwb_ctl.c @@ -0,0 +1,112 @@ +/*------------------------------------------------------------------------- + * + * dwb_ctl.c + * Shared-memory state and GUC variables of the short-lived double + * write buffer. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/storage/dwb/dwb_ctl.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "miscadmin.h" +#include "storage/dwb.h" +#include "storage/shmem.h" +#include "utils/guc.h" + +/* GUC variables (see 3.10 of the design plan) */ +int io_torn_pages_protection = DWB_PROTECT_FULL_PAGES; +int dwb_num_batches = 64; +int dwb_batch_pages = 64; +int dwb_max_segments = 4096; +int dwb_retire_workers = 1; +int dwb_batch_timeout_ms = 10; +int dwb_retire_interval_ms = 50; +bool dwb_writeback = true; +int dwb_slow_warn_ms = 5000; +int dwb_slot_stuck_timeout_ms = 30000; +int dwb_write_timeout_ms = 60000; +int dwb_on_stall = DWB_ON_STALL_PANIC; + +DWCtl *DWBCtl = NULL; +char *DWBStagingBase = NULL; + +static Size +DWBCtlSize(void) +{ + return offsetof(DWCtl, batches) + + mul_size(dwb_num_batches, sizeof(DWBatchCtl)); +} + +static Size +DWBStagingSize(void) +{ + /* IO-aligned staging buffers of one batch worth of pages each */ + return add_size(mul_size(DWB_STAGING_BUFFERS, + mul_size(dwb_batch_pages, BLCKSZ)), + PG_IO_ALIGN_SIZE); +} + +Size +DWBShmemSize(void) +{ + if (!DWBIsEnabled()) + return 0; + + return add_size(DWBCtlSize(), DWBStagingSize()); +} + +void +DWBShmemInit(void) +{ + bool found; + + if (!DWBIsEnabled()) + return; + + DWBCtl = (DWCtl *) ShmemInitStruct("DWB Ctl", DWBCtlSize(), &found); + + if (!found) + { + memset(DWBCtl, 0, DWBCtlSize()); + + for (int i = 0; i < DWB_NUM_WCLASSES; i++) + pg_atomic_init_u32(&DWBCtl->open_batch_idx[i], DWB_INVALID_BATCH); + pg_atomic_init_u64(&DWBCtl->next_batch_id, 1); + ConditionVariableInit(&DWBCtl->cv_free_batch); + ConditionVariableInit(&DWBCtl->cv_retire_wake); + SpinLockInit(&DWBCtl->staging_lock); + DWBCtl->staging_free = (1U << DWB_STAGING_BUFFERS) - 1; + + for (int i = 0; i < dwb_num_batches; i++) + { + DWBatchCtl *batch = &DWBCtl->batches[i]; + + pg_atomic_init_u32(&batch->state, DWB_FREE); + pg_atomic_init_u32(&batch->next_slot_idx, 0); + pg_atomic_init_u32(&batch->capped_slots, 0); + for (int w = 0; w < DWB_BITMAP_WORDS; w++) + pg_atomic_init_u64(&batch->slots_written_bitmap[w], 0); + pg_atomic_init_u32(&batch->ref_count, 0); + pg_atomic_init_u32(&batch->seg_pending_count, 0); + pg_atomic_init_u32(&batch->orphaned_refs_count, 0); + LWLockInitialize(&batch->publish_lock, LWTRANCHE_DWB_PUBLISH); + ConditionVariableInit(&batch->cv_state); + SpinLockInit(&batch->seg_lock); + batch->staging_idx = -1; + } + } + + { + char *base; + + base = (char *) ShmemInitStruct("DWB Staging", DWBStagingSize(), + &found); + DWBStagingBase = (char *) TYPEALIGN(PG_IO_ALIGN_SIZE, base); + } +} diff --git a/src/backend/storage/dwb/dwb_file.c b/src/backend/storage/dwb/dwb_file.c new file mode 100644 index 0000000000000..3025642b609f8 --- /dev/null +++ b/src/backend/storage/dwb/dwb_file.c @@ -0,0 +1,335 @@ +/*------------------------------------------------------------------------- + * + * dwb_file.c + * On-disk format of the short-lived double write buffer: ring creation + * and preallocation, control file, batch writes. + * + * Ring files are fully preallocated with zeros at creation (same contract + * as WAL segments): every block is allocated and the file size never + * changes, so batch durability only needs fdatasync. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/storage/dwb/dwb_file.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include +#include + +#include "common/file_utils.h" +#include "miscadmin.h" +#include "storage/dwb.h" +#include "storage/fd.h" +#include "utils/memutils.h" +#include "utils/wait_event.h" + +/* backend-local cache of open batch-file VFDs; only leaders and retire + * workers ever open batch files */ +static File *batch_files = NULL; + +pg_crc32c +DWBImageCrc(const char *image) +{ + pg_crc32c crc; + + INIT_CRC32C(crc); + COMP_CRC32C(crc, image, BLCKSZ); + FIN_CRC32C(crc); + return crc; +} + +pg_crc32c +DWBSlotMetaCrc(const DWSlotMeta *meta) +{ + pg_crc32c crc; + + INIT_CRC32C(crc); + COMP_CRC32C(crc, meta, offsetof(DWSlotMeta, meta_crc)); + FIN_CRC32C(crc); + return crc; +} + +pg_crc32c +DWBControlCrc(const DWBControlFileData *control) +{ + pg_crc32c crc; + + INIT_CRC32C(crc); + COMP_CRC32C(crc, control, offsetof(DWBControlFileData, crc)); + FIN_CRC32C(crc); + return crc; +} + +pg_crc32c +DWBBatchHeaderCrc(const DWBBatchHeader *hdr) +{ + pg_crc32c crc; + + INIT_CRC32C(crc); + COMP_CRC32C(crc, hdr, offsetof(DWBBatchHeader, crc)); + FIN_CRC32C(crc); + return crc; +} + +static void +DWBBatchFilePath(char *path, int batch_idx) +{ + snprintf(path, MAXPGPATH, DWB_DIR "/batch_%04d", batch_idx); +} + +/* + * Read pg_dwb/control. Returns false if the file does not exist and + * missing_ok; any other failure (including a CRC mismatch) is FATAL — + * a damaged control file must not silently degrade the apply-pass. + */ +bool +DWBReadControlFile(DWBControlFileData *control, bool missing_ok) +{ + int fd; + int r; + + fd = OpenTransientFile(DWB_CONTROL_FILE, O_RDONLY | PG_BINARY); + if (fd < 0) + { + if (errno == ENOENT && missing_ok) + return false; + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", DWB_CONTROL_FILE))); + } + + pgstat_report_wait_start(WAIT_EVENT_DWB_CONTROL_READ); + r = read(fd, control, sizeof(DWBControlFileData)); + pgstat_report_wait_end(); + if (r != sizeof(DWBControlFileData)) + ereport(FATAL, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("could not read file \"%s\": read %d of %zu", + DWB_CONTROL_FILE, r, sizeof(DWBControlFileData)))); + if (CloseTransientFile(fd) != 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", DWB_CONTROL_FILE))); + + if (control->magic != DWB_CONTROL_MAGIC || + !EQ_CRC32C(control->crc, DWBControlCrc(control))) + ereport(FATAL, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("invalid checksum or magic number in file \"%s\"", + DWB_CONTROL_FILE))); + if (control->min_version > DWB_VERSION) + ereport(FATAL, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("file \"%s\" requires format version at least %u, but this server supports %u", + DWB_CONTROL_FILE, control->min_version, DWB_VERSION))); + + return true; +} + +/* + * Write pg_dwb/control atomically: tmp file + fsync + durable_rename, + * all inside pg_dwb/. + */ +void +DWBWriteControlFile(const DWBControlFileData *control) +{ + const char *tmppath = DWB_DIR "/control.tmp"; + int fd; + + fd = OpenTransientFile(tmppath, + O_CREAT | O_TRUNC | O_WRONLY | PG_BINARY); + if (fd < 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not create file \"%s\": %m", tmppath))); + + pgstat_report_wait_start(WAIT_EVENT_DWB_CONTROL_WRITE); + errno = 0; + if (write(fd, control, sizeof(DWBControlFileData)) != + sizeof(DWBControlFileData)) + { + if (errno == 0) + errno = ENOSPC; + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not write file \"%s\": %m", tmppath))); + } + pgstat_report_wait_end(); + + pgstat_report_wait_start(WAIT_EVENT_DWB_CONTROL_SYNC); + if (pg_fsync(fd) != 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not fsync file \"%s\": %m", tmppath))); + pgstat_report_wait_end(); + + if (CloseTransientFile(fd) != 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", tmppath))); + + durable_rename(tmppath, DWB_CONTROL_FILE, FATAL); +} + +/* + * Create pg_dwb/ from scratch: directory, zero-preallocated batch files, + * control with generation 0. + */ +void +DWBCreateRing(void) +{ + DWBControlFileData control; + + if (MakePGDirectory(DWB_DIR) < 0 && errno != EEXIST) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not create directory \"%s\": %m", DWB_DIR))); + + for (int i = 0; i < dwb_num_batches; i++) + { + char path[MAXPGPATH]; + int fd; + int rc; + + DWBBatchFilePath(path, i); + fd = OpenTransientFile(path, O_CREAT | O_TRUNC | O_RDWR | PG_BINARY); + if (fd < 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not create file \"%s\": %m", path))); + + pgstat_report_wait_start(WAIT_EVENT_DWB_RING_INIT); + rc = pg_pwrite_zeros(fd, DWBBatchFileSize(dwb_batch_pages), 0); + if (rc < 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not write to file \"%s\": %m", path))); + if (pg_fsync(fd) != 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not fsync file \"%s\": %m", path))); + pgstat_report_wait_end(); + + if (CloseTransientFile(fd) != 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", path))); + } + + memset(&control, 0, sizeof(control)); + control.magic = DWB_CONTROL_MAGIC; + control.version = DWB_VERSION; + control.min_version = DWB_MIN_VERSION; + control.num_batches = dwb_num_batches; + control.batch_pages = dwb_batch_pages; + control.generation = 0; + control.crc = DWBControlCrc(&control); + DWBWriteControlFile(&control); + + fsync_fname(DWB_DIR, true); +} + +/* + * Return an open VFD for a batch file, from the backend-local cache. + */ +int +DWBOpenBatchFile(int batch_idx) +{ + char path[MAXPGPATH]; + + if (batch_files == NULL) + { + batch_files = (File *) + MemoryContextAllocZero(TopMemoryContext, + dwb_num_batches * sizeof(File)); + for (int i = 0; i < dwb_num_batches; i++) + batch_files[i] = -1; + } + + if (batch_files[batch_idx] >= 0) + return batch_files[batch_idx]; + + DWBBatchFilePath(path, batch_idx); + batch_files[batch_idx] = PathNameOpenFile(path, O_RDWR | PG_BINARY); + if (batch_files[batch_idx] < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", path))); + return batch_files[batch_idx]; +} + +void +DWBCloseBatchFiles(void) +{ + if (batch_files == NULL) + return; + for (int i = 0; i < dwb_num_batches; i++) + { + if (batch_files[i] >= 0) + { + FileClose(batch_files[i]); + batch_files[i] = -1; + } + } +} + +/* + * Leader write of one batch: (a) one contiguous pwrite of the image stream + * from staging, (b) one pwrite of the meta region, (c) fdatasync. Exactly + * this order: a crash while reusing a slot must never leave valid-looking + * meta over a torn or foreign image (any partially-persistent mix is + * rejected locally by meta_crc/generation/image_crc, see 3.2/3.4). + */ +void +DWBWriteBatch(int batch_idx, const DWBBatchHeader *hdr, + const DWSlotMeta *metas, const char *images) +{ + static char *meta_buf = NULL; + Size meta_region = DWBMetaRegionSize(dwb_batch_pages); + File file = DWBOpenBatchFile(batch_idx); + Size image_bytes = (Size) hdr->n_slots * BLCKSZ; + int rc; + + if (meta_buf == NULL) + meta_buf = MemoryContextAllocAligned(TopMemoryContext, meta_region, + PG_IO_ALIGN_SIZE, 0); + + rc = FileWrite(file, images, image_bytes, meta_region, + WAIT_EVENT_DWB_BATCH_WRITE); + if (rc != image_bytes) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not write batch %d of \"%s\": %m", + batch_idx, DWB_DIR))); + + memset(meta_buf, 0, meta_region); + memcpy(meta_buf, hdr, sizeof(DWBBatchHeader)); + memcpy(meta_buf + sizeof(DWBBatchHeader), metas, + hdr->n_slots * sizeof(DWSlotMeta)); + + rc = FileWrite(file, meta_buf, meta_region, 0, + WAIT_EVENT_DWB_BATCH_WRITE); + if (rc != meta_region) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not write batch %d of \"%s\": %m", + batch_idx, DWB_DIR))); + + /* + * fdatasync suffices: the file was fully preallocated at ring creation, + * its size and block layout never change (WAL-segment contract). + */ + pgstat_report_wait_start(WAIT_EVENT_DWB_BATCH_SYNC); + rc = FileGetRawDesc(file); + if (rc < 0 || pg_fdatasync(rc) != 0) + ereport(data_sync_elevel(ERROR), + (errcode_for_file_access(), + errmsg("could not fsync batch %d of \"%s\": %m", + batch_idx, DWB_DIR))); + pgstat_report_wait_end(); +} diff --git a/src/backend/storage/dwb/dwb_recovery.c b/src/backend/storage/dwb/dwb_recovery.c new file mode 100644 index 0000000000000..039acab14c53c --- /dev/null +++ b/src/backend/storage/dwb/dwb_recovery.c @@ -0,0 +1,81 @@ +/*------------------------------------------------------------------------- + * + * dwb_recovery.c + * Startup-time handling of the short-lived double write buffer ring. + * + * On every start (clean, unclean or cold) the durable generation in + * pg_dwb/control is bumped BEFORE the ring opens for new writes, so slots + * left behind by the previous run can never masquerade as current after a + * future crash. Order: read G -> (unclean start, Stage 4) apply-pass over + * generation G + fsync -> durable control.generation := G+1 -> open ring. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/storage/dwb/dwb_recovery.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/xlog.h" +#include "storage/dwb.h" + +/* + * Called from StartupXLOG before WAL replay. Creates or validates the + * ring, enforces data checksums, performs the durable generation bump and + * publishes ring_generation for the leaders' slot metas. + */ +void +DWBStartup(void) +{ + DWBControlFileData control; + + if (!DWBIsEnabled()) + return; + + /* 3.1.7: a torn page with an intact header must never pass unnoticed */ + if (!DataChecksumsEnabled()) + ereport(FATAL, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("io_torn_pages_protection = \"double_writes\" requires data checksums"), + errhint("Enable checksums with initdb -k or pg_checksums."))); + + if (!DWBReadControlFile(&control, true)) + { + /* cold start: no ring yet */ + DWBCreateRing(); + if (!DWBReadControlFile(&control, false)) + pg_unreachable(); + } + else if (control.num_batches != (uint32) dwb_num_batches || + control.batch_pages != (uint32) dwb_batch_pages) + { + /* + * Geometry GUCs define the on-disk layout. Re-creating the ring + * under a changed geometry must not skip the apply-pass over the + * old ring, so it is deferred to Stage 4; until then, refuse. + */ + ereport(FATAL, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"%s\" was created with dwb_num_batches = %u and dwb_batch_pages = %u", + DWB_DIR, control.num_batches, control.batch_pages), + errhint("Restore the previous settings."))); + } + + /* + * Stage 4: on an unclean start the apply-pass over generation + * control.generation runs here, before the bump. + */ + + control.generation++; + control.crc = DWBControlCrc(&control); + DWBWriteControlFile(&control); + + DWBCtl->ring_generation = control.generation; + + ereport(LOG, + (errmsg("double write buffer ring opened: %d batches of %d pages, generation " UINT64_FORMAT, + dwb_num_batches, dwb_batch_pages, control.generation))); +} diff --git a/src/backend/storage/dwb/meson.build b/src/backend/storage/dwb/meson.build new file mode 100644 index 0000000000000..e0a4ac73f2a1b --- /dev/null +++ b/src/backend/storage/dwb/meson.build @@ -0,0 +1,8 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +backend_sources += files( + 'dwb.c', + 'dwb_ctl.c', + 'dwb_file.c', + 'dwb_recovery.c', +) diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 8d2b0f1193d9f..fd8f78ac1dedb 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -39,6 +39,7 @@ #include "replication/walsender.h" #include "storage/aio_subsys.h" #include "storage/bufmgr.h" +#include "storage/dwb.h" #include "storage/dsm.h" #include "storage/dsm_registry.h" #include "storage/ipc.h" @@ -114,6 +115,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, dsm_estimate_size()); size = add_size(size, DSMRegistryShmemSize()); size = add_size(size, BufferManagerShmemSize()); + size = add_size(size, DWBShmemSize()); size = add_size(size, LockManagerShmemSize()); size = add_size(size, PredicateLockShmemSize()); size = add_size(size, ProcGlobalShmemSize()); @@ -293,6 +295,7 @@ CreateOrAttachShmemStructs(void) SUBTRANSShmemInit(); MultiXactShmemInit(); BufferManagerShmemInit(); + DWBShmemInit(); /* * Set up lock manager diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c index c3d4b7275ecc1..1c53553aa2508 100644 --- a/src/backend/storage/lmgr/lwlock.c +++ b/src/backend/storage/lmgr/lwlock.c @@ -178,6 +178,7 @@ static const char *const BuiltinTrancheNames[] = { [LWTRANCHE_XACT_SLRU] = "XactSLRU", [LWTRANCHE_PARALLEL_VACUUM_DSA] = "ParallelVacuumDSA", [LWTRANCHE_AIO_URING_COMPLETION] = "AioUringCompletion", + [LWTRANCHE_DWB_PUBLISH] = "DWBPublish", }; StaticAssertDecl(lengthof(BuiltinTrancheNames) == diff --git a/src/backend/storage/meson.build b/src/backend/storage/meson.build index 0cd48844f1d9b..52ce2bbdc4548 100644 --- a/src/backend/storage/meson.build +++ b/src/backend/storage/meson.build @@ -2,6 +2,7 @@ subdir('aio') subdir('buffer') +subdir('dwb') subdir('file') subdir('freespace') subdir('ipc') diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index b9c1e6900ec1b..9a0b7d2aef71c 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -116,6 +116,9 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting." CHECKPOINT_DONE "Waiting for a checkpoint to complete." CHECKPOINT_START "Waiting for a checkpoint to start." +DWB_BATCH_COVERAGE "Waiting for writers of a double write buffer batch to publish their page images." +DWB_BATCH_FSYNC "Waiting for a double write buffer batch to reach durable storage." +DWB_FREE_BATCH "Waiting for a free double write buffer batch." EXECUTE_GATHER "Waiting for activity from a child process while executing a Gather plan node." HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table." HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table." @@ -219,6 +222,12 @@ DATA_FILE_TRUNCATE "Waiting for a relation data file to be truncated." DATA_FILE_WRITE "Waiting for a write to a relation data file." DSM_ALLOCATE "Waiting for a dynamic shared memory segment to be allocated." DSM_FILL_ZERO_WRITE "Waiting to fill a dynamic shared memory backing file with zeroes." +DWB_BATCH_SYNC "Waiting for a double write buffer batch file to reach durable storage." +DWB_BATCH_WRITE "Waiting for a write to a double write buffer batch file." +DWB_CONTROL_READ "Waiting for a read of the double write buffer control file." +DWB_CONTROL_SYNC "Waiting for the double write buffer control file to reach durable storage." +DWB_CONTROL_WRITE "Waiting for a write to the double write buffer control file." +DWB_RING_INIT "Waiting for preallocation of the double write buffer ring files." LOCK_FILE_ADDTODATADIR_READ "Waiting for a read while adding a line to the data directory lock file." LOCK_FILE_ADDTODATADIR_SYNC "Waiting for data to reach durable storage while adding a line to the data directory lock file." LOCK_FILE_ADDTODATADIR_WRITE "Waiting for a write while adding a line to the data directory lock file." @@ -352,6 +361,7 @@ DSMRegistry "Waiting to read or update the dynamic shared memory registry." InjectionPoint "Waiting to read or update information related to injection points." SerialControl "Waiting to read or update shared pg_serial state." AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." +DWBRingOpen "Waiting to open a new double write buffer batch." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) @@ -402,6 +412,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +DWBPublish "Waiting to publish a double write buffer batch's segment set." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 6b82a23435efe..e93e0fd4ac082 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -78,6 +78,7 @@ #include "replication/syncrep.h" #include "storage/aio.h" #include "storage/bufmgr.h" +#include "storage/dwb.h" #include "storage/bufpage.h" #include "storage/copydir.h" #include "storage/fd.h" @@ -359,6 +360,20 @@ static const struct config_enum_entry synchronous_commit_options[] = { * Although only "on", "off", "try" are documented, we accept all the likely * variants of "on" and "off". */ +static const struct config_enum_entry io_torn_pages_protection_options[] = { + {"off", DWB_PROTECT_OFF, false}, + {"full_pages", DWB_PROTECT_FULL_PAGES, false}, + {"double_writes", DWB_PROTECT_DOUBLE_WRITES, false}, + {NULL, 0, false} +}; + +static const struct config_enum_entry dwb_on_stall_options[] = { + {"warn", DWB_ON_STALL_WARN, false}, + {"error", DWB_ON_STALL_ERROR, false}, + {"panic", DWB_ON_STALL_PANIC, false}, + {NULL, 0, false} +}; + static const struct config_enum_entry huge_pages_options[] = { {"off", HUGE_PAGES_OFF, false}, {"on", HUGE_PAGES_ON, false}, @@ -1204,6 +1219,15 @@ struct config_bool ConfigureNamesBool[] = true, NULL, NULL, NULL }, + { + {"dwb_writeback", PGC_SIGHUP, WAL_SETTINGS, + gettext_noop("Starts kernel writeback of data pages right after a double write buffer write."), + gettext_noop("Makes the retire fsync a cheap barrier instead of a full flush.") + }, + &dwb_writeback, + true, + NULL, NULL, NULL + }, { {"wal_log_hints", PGC_POSTMASTER, WAL_SETTINGS, @@ -2172,6 +2196,92 @@ struct config_int ConfigureNamesInt[] = 0, 0, INT_MAX / 2, NULL, NULL, NULL }, + { + {"dwb_num_batches", PGC_POSTMASTER, WAL_SETTINGS, + gettext_noop("Number of batches in the double write buffer ring."), + NULL + }, + &dwb_num_batches, + 64, 16, 1024, + NULL, NULL, NULL + }, + { + {"dwb_batch_pages", PGC_POSTMASTER, WAL_SETTINGS, + gettext_noop("Number of pages per double write buffer batch."), + NULL + }, + &dwb_batch_pages, + 64, 16, 256, + NULL, NULL, NULL + }, + { + {"dwb_max_segments", PGC_POSTMASTER, WAL_SETTINGS, + gettext_noop("Capacity of the double write buffer segment hash table."), + NULL + }, + &dwb_max_segments, + 4096, 1024, 1048576, + NULL, NULL, NULL + }, + { + {"dwb_retire_workers", PGC_POSTMASTER, WAL_SETTINGS, + gettext_noop("Number of double write buffer retire worker processes."), + NULL + }, + &dwb_retire_workers, + 1, 1, 32, + NULL, NULL, NULL + }, + { + {"dwb_batch_timeout_ms", PGC_SIGHUP, WAL_SETTINGS, + gettext_noop("Maximum time an open double write buffer batch may wait before being sealed."), + NULL, + GUC_UNIT_MS + }, + &dwb_batch_timeout_ms, + 10, 1, 1000, + NULL, NULL, NULL + }, + { + {"dwb_retire_interval_ms", PGC_SIGHUP, WAL_SETTINGS, + gettext_noop("Cycle time of each double write buffer retire worker."), + NULL, + GUC_UNIT_MS + }, + &dwb_retire_interval_ms, + 50, 5, 5000, + NULL, NULL, NULL + }, + { + {"dwb_slow_warn_ms", PGC_SIGHUP, WAL_SETTINGS, + gettext_noop("Double write buffer wait time after which throttling of non-critical writers begins."), + NULL, + GUC_UNIT_MS + }, + &dwb_slow_warn_ms, + 5000, 100, 60000, + NULL, NULL, NULL + }, + { + {"dwb_slot_stuck_timeout_ms", PGC_SIGHUP, WAL_SETTINGS, + gettext_noop("Time a double write buffer batch leader waits for slot coverage before PANIC."), + NULL, + GUC_UNIT_MS + }, + &dwb_slot_stuck_timeout_ms, + 30000, 1000, 600000, + NULL, NULL, NULL + }, + { + {"dwb_write_timeout_ms", PGC_SIGHUP, WAL_SETTINGS, + gettext_noop("Double write buffer wait time after which dwb_on_stall applies."), + NULL, + GUC_UNIT_MS + }, + &dwb_write_timeout_ms, + 60000, 1000, 600000, + NULL, NULL, NULL + }, { {"post_auth_delay", PGC_BACKEND, DEVELOPER_OPTIONS, gettext_noop("Sets the amount of time to wait after " @@ -5003,6 +5113,28 @@ struct config_string ConfigureNamesString[] = struct config_enum ConfigureNamesEnum[] = { + { + {"io_torn_pages_protection", PGC_POSTMASTER, WAL_SETTINGS, + gettext_noop("Selects the protection against torn (partially written) data pages."), + gettext_noop("\"full_pages\" writes full page images to WAL after a checkpoint, " + "\"double_writes\" uses the double write buffer in pg_dwb, " + "\"off\" disables protection.") + }, + &io_torn_pages_protection, + DWB_PROTECT_FULL_PAGES, io_torn_pages_protection_options, + NULL, NULL, NULL + }, + + { + {"dwb_on_stall", PGC_SIGHUP, WAL_SETTINGS, + gettext_noop("Action to take when a double write buffer wait exceeds dwb_write_timeout_ms."), + NULL + }, + &dwb_on_stall, + DWB_ON_STALL_PANIC, dwb_on_stall_options, + NULL, NULL, NULL + }, + { {"backslash_quote", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS, gettext_noop("Sets whether \"\\'\" is allowed in string literals."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index d91133dbd7357..75cafa0b8d571 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -247,6 +247,23 @@ # fsync_writethrough # open_sync #full_page_writes = on # recover from partial page writes +#io_torn_pages_protection = full_pages # off, full_pages, or double_writes + # (change requires restart) +#dwb_num_batches = 64 # batches in the double write ring + # (change requires restart) +#dwb_batch_pages = 64 # pages per batch + # (change requires restart) +#dwb_max_segments = 4096 # segment hash capacity + # (change requires restart) +#dwb_retire_workers = 1 # retire worker processes + # (change requires restart) +#dwb_batch_timeout_ms = 10ms # force-seal an open batch after this time +#dwb_retire_interval_ms = 50ms # retire worker cycle +#dwb_writeback = on # start kernel writeback after batch writes +#dwb_slow_warn_ms = 5s # throttle non-critical writers after this wait +#dwb_slot_stuck_timeout_ms = 30s # PANIC on stuck batch coverage +#dwb_write_timeout_ms = 60s # apply dwb_on_stall after this wait +#dwb_on_stall = panic # panic, error, or warn #wal_log_hints = off # also do full page writes of non-critical updates # (change requires restart) #wal_compression = off # enables compression of full-page writes; diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h new file mode 100644 index 0000000000000..79b1bb5259ed1 --- /dev/null +++ b/src/include/storage/dwb.h @@ -0,0 +1,262 @@ +/*------------------------------------------------------------------------- + * + * dwb.h + * Short-lived double write buffer (DWB). + * + * A DWB slot lives for the duration of one flush batch and is reused only + * after the data-file fsync covering its page has become durable. Strict + * durability order per batch: + * + * XLogFlush(page LSN) -> batch write + fdatasync (leader) -> + * smgrwrite (kernel cache) -> eventual segment fsync -> slot reuse + * + * See .plan/short-lived-dwb_REL_18.md for the full design. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/storage/dwb.h + * + *------------------------------------------------------------------------- + */ +#ifndef DWB_H +#define DWB_H + +#include "access/xlogdefs.h" +#include "port/pg_crc32c.h" +#include "storage/buf_internals.h" +#include "storage/condition_variable.h" +#include "storage/lwlock.h" +#include "storage/relfilelocator.h" +#include "storage/s_lock.h" +#include "utils/timestamp.h" + +/* GUC: io_torn_pages_protection */ +typedef enum +{ + DWB_PROTECT_OFF, + DWB_PROTECT_FULL_PAGES, + DWB_PROTECT_DOUBLE_WRITES, +} DWBTornPageProtection; + +/* GUC: dwb_on_stall (Stage B backpressure behaviour) */ +typedef enum +{ + DWB_ON_STALL_WARN, + DWB_ON_STALL_ERROR, + DWB_ON_STALL_PANIC, +} DWBOnStall; + +/* GUC variables (defined in dwb_ctl.c) */ +extern PGDLLIMPORT int io_torn_pages_protection; +extern PGDLLIMPORT int dwb_num_batches; +extern PGDLLIMPORT int dwb_batch_pages; +extern PGDLLIMPORT int dwb_max_segments; +extern PGDLLIMPORT int dwb_retire_workers; +extern PGDLLIMPORT int dwb_batch_timeout_ms; +extern PGDLLIMPORT int dwb_retire_interval_ms; +extern PGDLLIMPORT bool dwb_writeback; +extern PGDLLIMPORT int dwb_slow_warn_ms; +extern PGDLLIMPORT int dwb_slot_stuck_timeout_ms; +extern PGDLLIMPORT int dwb_write_timeout_ms; +extern PGDLLIMPORT int dwb_on_stall; + +#define DWBIsEnabled() (io_torn_pages_protection == DWB_PROTECT_DOUBLE_WRITES) + +/* + * Compile-time capacity limits (GUC maxima). + */ +#define DWB_BATCH_MAX_PAGES 256 +#define DWB_BATCH_MAX_SEGS DWB_BATCH_MAX_PAGES +#define DWB_BITMAP_WORDS (DWB_BATCH_MAX_PAGES / 64) +/* staging pool: 2 writer classes + 2 in-flight leader writes */ +#define DWB_STAGING_BUFFERS 4 +/* writer classes (3.6); Stage 1 uses only DWB_WCLASS_EVICTION */ +#define DWB_NUM_WCLASSES 2 +#define DWB_WCLASS_EVICTION 0 +#define DWB_WCLASS_BACKGROUND 1 + +#define DWB_DIR "pg_dwb" +#define DWB_CONTROL_FILE DWB_DIR "/control" + +/* + * On-disk format. + * + * pg_dwb/control - geometry + durable generation, written atomically + * pg_dwb/batch_NNNN - meta region (header + slot metas, padded to + * PG_IO_ALIGN_SIZE) followed by a contiguous + * BLCKSZ-aligned page-image stream + * + * Slot validity is locally verifiable: meta_crc rejects a torn meta write + * (including any old/new field mix on slot reuse), image_crc rejects a torn + * image; an apply-pass candidate must pass meta_crc + generation + image_crc. + */ +#define DWB_CONTROL_MAGIC 0x44574243 /* "DWBC" */ +#define DWB_BATCH_MAGIC 0x44574242 /* "DWBB" */ +#define DWB_VERSION 1 +#define DWB_MIN_VERSION 1 + +typedef struct DWBControlFileData +{ + uint32 magic; + uint32 version; + uint32 min_version; + uint32 num_batches; + uint32 batch_pages; + uint64 generation; /* apply-pass horizon: bumped durably on + * every start before the ring opens */ + pg_crc32c crc; /* CRC of all preceding fields */ +} DWBControlFileData; + +typedef struct DWBBatchHeader +{ + uint32 magic; + uint32 version; + uint64 batch_id; + uint32 n_slots; /* capped_slots at seal time */ + pg_crc32c crc; /* CRC of all preceding fields */ +} DWBBatchHeader; + +typedef struct DWSlotMeta +{ + BufferTag tag; + XLogRecPtr page_lsn; + uint64 generation; /* ring generation at write time */ + uint16 flags; + pg_crc32c image_crc; /* CRC of the BLCKSZ page image */ + pg_crc32c meta_crc; /* CRC of all preceding fields */ +} DWSlotMeta; + +/* DWSlotMeta.flags */ +#define DWB_SLOT_ABORTED 0x0001 /* writer died before publishing */ + +#define DWBMetaRegionSize(batch_pages) \ + TYPEALIGN(PG_IO_ALIGN_SIZE, \ + sizeof(DWBBatchHeader) + (batch_pages) * sizeof(DWSlotMeta)) +#define DWBBatchFileSize(batch_pages) \ + (DWBMetaRegionSize(batch_pages) + (Size) (batch_pages) * BLCKSZ) + +/* + * Batch lifecycle. A slot is reused only via DWB_FREE. + */ +typedef enum DWBatchState +{ + DWB_FREE = 0, + DWB_ALLOCATED, /* writers fill slots */ + DWB_SEALED, /* no new writers; waiting for bitmap + * coverage, then the leader writes */ + DWB_WRITTEN, /* images + meta written, fdatasync pending */ + DWB_FSYNCED, /* batch durable; writers do smgrwrite */ + DWB_DATA_WRITTEN, /* all smgrwrite + sync requests done */ + DWB_RETIRING, /* waiting for fsync of seg_set segments */ + DWB_OOM_RETIRING, /* publisher retires synchronously (3.5) */ +} DWBatchState; + +typedef struct DWSegRef +{ + RelFileLocator rlocator; + ForkNumber forknum; + uint32 segno; +} DWSegRef; + +/* + * next_slot_idx encoding: 31-bit index + seal sentinel bit. + */ +#define DWB_SEAL_BIT (1U << 31) +#define DWB_IDX_MASK (DWB_SEAL_BIT - 1) + +typedef struct DWBatchCtl +{ + pg_atomic_uint32 state; /* DWBatchState */ + pg_atomic_uint32 next_slot_idx; /* fetch_add on ALLOCATED */ + pg_atomic_uint32 capped_slots; /* fixed by SEAL; leader waits for + * exactly this many bitmap bits */ + pg_atomic_uint64 slots_written_bitmap[DWB_BITMAP_WORDS]; + pg_atomic_uint32 ref_count; /* writers holding the batch from slot + * reservation to smgrwrite done */ + pg_atomic_uint32 seg_pending_count; /* seg_set entries not yet fsynced */ + pg_atomic_uint32 orphaned_refs_count; /* refs whose writer aborted after + * publishing the copy but before + * smgrwrite; a retire worker + * finishes their writes */ + LWLock publish_lock; /* serializes seg_set publication and + * seg_pending_count decrement (3.5) */ + ConditionVariable cv_state; /* broadcast on state change */ + slock_t seg_lock; /* protects n_segs/seg_set dedup insert */ + uint32 n_segs; + DWSegRef seg_set[DWB_BATCH_MAX_SEGS]; + BufferTag pages[DWB_BATCH_MAX_PAGES]; + XLogRecPtr page_lsns[DWB_BATCH_MAX_PAGES]; + pg_crc32c image_crcs[DWB_BATCH_MAX_PAGES]; /* computed by writers at + * publication */ + uint8 slot_flags[DWB_BATCH_MAX_PAGES]; + int staging_idx; /* staging buffer; held from ALLOCATED until + * the leader finishes the image pwrite */ + BufferTag orphan_tags[DWB_BATCH_MAX_PAGES]; + XLogRecPtr max_page_lsn; + uint64 batch_id; /* monotonic, for ordering */ + TimestampTz open_time; /* FREE -> ALLOCATED instant; drives + * force-SEAL via dwb_batch_timeout_ms */ +} DWBatchCtl; + +typedef struct DWCtl +{ + pg_atomic_uint32 open_batch_idx[DWB_NUM_WCLASSES]; /* current ALLOCATED + * batch per writer + * class, or + * DWB_INVALID_BATCH */ + pg_atomic_uint64 next_batch_id; + uint64 ring_generation; /* = control.generation after the startup + * bump; constant until restart, stamped + * into DWSlotMeta by the leader */ + ConditionVariable cv_free_batch; /* broadcast on retire */ + ConditionVariable cv_retire_wake; /* wakes retire workers */ + slock_t staging_lock; /* protects staging_free bitmap */ + uint32 staging_free; /* bitmap of free staging buffers */ + DWBatchCtl batches[FLEXIBLE_ARRAY_MEMBER]; /* dwb_num_batches entries */ +} DWCtl; + +#define DWB_INVALID_BATCH PG_UINT32_MAX + +/* Writer-side handle for one reserved slot */ +typedef struct DWBSlotRef +{ + int batch_idx; + int slot_idx; + uint64 batch_id; +} DWBSlotRef; + +extern PGDLLIMPORT DWCtl *DWBCtl; +extern PGDLLIMPORT char *DWBStagingBase; + +/* dwb_ctl.c */ +extern Size DWBShmemSize(void); +extern void DWBShmemInit(void); + +/* dwb.c — write path (Stage 1: driven by tests, not FlushBuffer yet) */ +extern void DWBAcquireSlot(const BufferTag *tag, DWBSlotRef *ref); +extern void DWBPublishImage(const DWBSlotRef *ref, const char *image, + XLogRecPtr page_lsn); +extern void DWBWaitBatchFsynced(const DWBSlotRef *ref); +extern void DWBReleaseSlot(const DWBSlotRef *ref); +extern bool DWBForceSealOpenBatch(int wclass); +extern int DWBRetireAllSync(void); +extern DWBatchState DWBGetBatchState(int batch_idx); + +/* dwb_file.c */ +extern void DWBCreateRing(void); +extern bool DWBReadControlFile(DWBControlFileData *control, bool missing_ok); +extern void DWBWriteControlFile(const DWBControlFileData *control); +extern int DWBOpenBatchFile(int batch_idx); +extern void DWBCloseBatchFiles(void); +extern void DWBWriteBatch(int batch_idx, const DWBBatchHeader *hdr, + const DWSlotMeta *metas, const char *images); +extern pg_crc32c DWBImageCrc(const char *image); +extern pg_crc32c DWBSlotMetaCrc(const DWSlotMeta *meta); +extern pg_crc32c DWBControlCrc(const DWBControlFileData *control); +extern pg_crc32c DWBBatchHeaderCrc(const DWBBatchHeader *hdr); + +/* dwb_recovery.c */ +extern void DWBStartup(void); + +#endif /* DWB_H */ diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h index 08a72569ae5fd..c65ea79edadab 100644 --- a/src/include/storage/lwlock.h +++ b/src/include/storage/lwlock.h @@ -221,6 +221,7 @@ typedef enum BuiltinTrancheIds LWTRANCHE_XACT_SLRU, LWTRANCHE_PARALLEL_VACUUM_DSA, LWTRANCHE_AIO_URING_COMPLETION, + LWTRANCHE_DWB_PUBLISH, LWTRANCHE_FIRST_USER_DEFINED, } BuiltinTrancheIds; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 932024b1b0ba5..671e84e4c24ee 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -84,3 +84,4 @@ PG_LWLOCK(50, DSMRegistry) PG_LWLOCK(51, InjectionPoint) PG_LWLOCK(52, SerialControl) PG_LWLOCK(53, AioWorkerSubmissionQueue) +PG_LWLOCK(54, DWBRingOpen) diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 4e82d6f151732..93251cc6e90ac 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -23,6 +23,7 @@ SUBDIRS = \ test_ddl_deparse \ test_dsa \ test_dsm_registry \ + test_dwb \ test_escape \ test_extensions \ test_ginpostinglist \ diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 9a957351ab693..846fd4a9e0015 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -22,6 +22,7 @@ subdir('test_custom_types') subdir('test_ddl_deparse') subdir('test_dsa') subdir('test_dsm_registry') +subdir('test_dwb') subdir('test_escape') subdir('test_extensions') subdir('test_ginpostinglist') diff --git a/src/test/modules/test_dwb/.gitignore b/src/test/modules/test_dwb/.gitignore new file mode 100644 index 0000000000000..5dcb3ff972350 --- /dev/null +++ b/src/test/modules/test_dwb/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_dwb/Makefile b/src/test/modules/test_dwb/Makefile new file mode 100644 index 0000000000000..e2234553e094d --- /dev/null +++ b/src/test/modules/test_dwb/Makefile @@ -0,0 +1,29 @@ +# src/test/modules/test_dwb/Makefile + +MODULE_big = test_dwb +OBJS = \ + $(WIN32RES) \ + test_dwb.o +PGFILEDESC = "test_dwb - test module for the short-lived double write buffer" + +TAP_TESTS = 1 + +EXTENSION = test_dwb +DATA = test_dwb--1.0.sql + +REGRESS_OPTS = --temp-config $(top_srcdir)/src/test/modules/test_dwb/test_dwb.conf +REGRESS = test_dwb +# Disabled because these tests require io_torn_pages_protection=double_writes, +# which typical installcheck users do not have. +NO_INSTALLCHECK = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_dwb +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_dwb/expected/test_dwb.out b/src/test/modules/test_dwb/expected/test_dwb.out new file mode 100644 index 0000000000000..54773d786d255 --- /dev/null +++ b/src/test/modules/test_dwb/expected/test_dwb.out @@ -0,0 +1,38 @@ +CREATE EXTENSION test_dwb; +-- ring is idle after startup +SELECT test_dwb_states(); + test_dwb_states +---------------------------------------------------------------------------- + free=16 allocated=0 sealed=0 written=0 fsynced=0 data_written=0 retiring=0 +(1 row) + +-- a partial batch, sealed by force (ring: 16 batches x 16 pages) +SELECT test_dwb_cycle(5); + test_dwb_cycle +---------------- + 1 +(1 row) + +-- overflow-sealed batches: 40 pages = 16 + 16 + 8 (tail force-sealed) +SELECT test_dwb_cycle(40); + test_dwb_cycle +---------------- + 3 +(1 row) + +-- every surviving slot validates against meta_crc, generation and +-- image_crc: eager retirement lets the cycles reuse batch file 0, so its +-- final content is the 8-slot tail write, plus 16 slots in batch file 1 +SELECT test_dwb_ring_slots(true); + test_dwb_ring_slots +--------------------- + 24 +(1 row) + +-- and the ring is fully retired again +SELECT test_dwb_states(); + test_dwb_states +---------------------------------------------------------------------------- + free=16 allocated=0 sealed=0 written=0 fsynced=0 data_written=0 retiring=0 +(1 row) + diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build new file mode 100644 index 0000000000000..c3dd8779096ec --- /dev/null +++ b/src/test/modules/test_dwb/meson.build @@ -0,0 +1,40 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +test_dwb_sources = files( + 'test_dwb.c', +) + +if host_system == 'windows' + test_dwb_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_dwb', + '--FILEDESC', 'test_dwb - test module for the short-lived double write buffer',]) +endif + +test_dwb = shared_module('test_dwb', + test_dwb_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_dwb + +test_install_data += files( + 'test_dwb.control', + 'test_dwb--1.0.sql', +) + +tests += { + 'name': 'test_dwb', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_dwb', + ], + 'regress_args': ['--temp-config', files('test_dwb.conf')], + 'runningcheck': false, + }, + 'tap': { + 'tests': [ + 't/001_dwb.pl' + ], + }, +} diff --git a/src/test/modules/test_dwb/sql/test_dwb.sql b/src/test/modules/test_dwb/sql/test_dwb.sql new file mode 100644 index 0000000000000..50aa384632757 --- /dev/null +++ b/src/test/modules/test_dwb/sql/test_dwb.sql @@ -0,0 +1,18 @@ +CREATE EXTENSION test_dwb; + +-- ring is idle after startup +SELECT test_dwb_states(); + +-- a partial batch, sealed by force (ring: 16 batches x 16 pages) +SELECT test_dwb_cycle(5); + +-- overflow-sealed batches: 40 pages = 16 + 16 + 8 (tail force-sealed) +SELECT test_dwb_cycle(40); + +-- every surviving slot validates against meta_crc, generation and +-- image_crc: eager retirement lets the cycles reuse batch file 0, so its +-- final content is the 8-slot tail write, plus 16 slots in batch file 1 +SELECT test_dwb_ring_slots(true); + +-- and the ring is fully retired again +SELECT test_dwb_states(); diff --git a/src/test/modules/test_dwb/t/001_dwb.pl b/src/test/modules/test_dwb/t/001_dwb.pl new file mode 100644 index 0000000000000..b227fd35de00c --- /dev/null +++ b/src/test/modules/test_dwb/t/001_dwb.pl @@ -0,0 +1,83 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Concurrency, restart (generation) and enforcement tests for the +# short-lived double write buffer. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +)); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); + +# --- single-session cycles --------------------------------------------- + +is( $node->safe_psql('postgres', 'SELECT test_dwb_cycle(40)'), + '3', 'overflow-sealed cycle retires three batches'); +# eager retirement reuses batch file 0 within the cycle: its final content +# is the 8-slot tail write, plus 16 slots in batch file 1 +is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), + '24', 'all surviving slots validate (meta_crc, generation, image_crc)'); + +# --- concurrent writers over a small ring ------------------------------ + +# 3 clients x 30 transactions x 40 pages through a 16x16 ring +my $script = $node->basedir . '/stress.sql'; +open my $fh, '>', $script or die $!; +print $fh "SELECT test_dwb_stress(1, 40);\n"; +close $fh; +$node->command_ok( + [ 'pgbench', '-n', '-c', '3', '-j', '3', '-t', '30', '-f', $script, + 'postgres' ], + 'concurrent stress over a small ring'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/free=16 allocated=0 sealed=0 written=0 fsynced=0 data_written=0 retiring=0/, + 'ring fully retired after concurrent stress'); +cmp_ok($node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), + '>', 0, 'ring holds valid current-generation slots after stress'); + +# --- restart bumps the durable generation ------------------------------ + +my $stale = $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(false)'); +$node->restart; +is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), + '0', 'no slot belongs to the new generation after restart'); +is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(false)'), + $stale, 'stale slots still CRC-valid, only the generation gates them'); + +# --- geometry is fixed by the on-disk control file --------------------- + +$node->stop; +$node->append_conf('postgresql.conf', 'dwb_num_batches = 32'); +my $ret = $node->start(fail_ok => 1); +is($ret, 0, 'start refused after geometry change'); +ok( $node->log_contains('was created with dwb_num_batches = 16'), + 'geometry mismatch reported'); +$node->append_conf('postgresql.conf', 'dwb_num_batches = 16'); +$node->start; +$node->stop; + +# --- double_writes requires data checksums ----------------------------- + +my $node2 = PostgreSQL::Test::Cluster->new('dwb_nochecksums'); +$node2->init(extra => ['--no-data-checksums']); +$node2->append_conf('postgresql.conf', + 'io_torn_pages_protection = double_writes'); +$ret = $node2->start(fail_ok => 1); +is($ret, 0, 'start refused without data checksums'); +ok( $node2->log_contains('requires data checksums'), + 'checksum requirement reported'); + +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql new file mode 100644 index 0000000000000..75e876825309c --- /dev/null +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -0,0 +1,20 @@ +/* src/test/modules/test_dwb/test_dwb--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_dwb" to load this file. \quit + +CREATE FUNCTION test_dwb_cycle(npages int) + RETURNS int STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_stress(loops int, npages int) + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_ring_slots(current_only bool) + RETURNS int STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_states() + RETURNS text STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c new file mode 100644 index 0000000000000..53478228117ec --- /dev/null +++ b/src/test/modules/test_dwb/test_dwb.c @@ -0,0 +1,237 @@ +/*-------------------------------------------------------------------------- + * + * test_dwb.c + * Test module for the short-lived double write buffer. + * + * Drives the DWB batch state machine directly (Stage 1: FlushBuffer is not + * wired in yet) with synthetic page tags and images, and validates the + * on-disk ring format independently of the server-side write path. + * + * Copyright (c) 2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_dwb/test_dwb.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include +#include + +#include "catalog/pg_tablespace_d.h" +#include "fmgr.h" +#include "miscadmin.h" +#include "storage/dwb.h" +#include "storage/fd.h" +#include "utils/builtins.h" + +PG_MODULE_MAGIC; + +static void +check_dwb_enabled(void) +{ + if (!DWBIsEnabled()) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("test_dwb requires io_torn_pages_protection = \"double_writes\""))); +} + +static void +wait_and_release(DWBSlotRef *refs, int nrefs) +{ + for (int i = 0; i < nrefs; i++) + { + DWBWaitBatchFsynced(&refs[i]); + DWBReleaseSlot(&refs[i]); + } +} + +/* + * One full write cycle over npages synthetic pages: acquire, publish, + * seal (by overflow or forced), wait durable, release, retire. + * Returns the number of batches retired. + */ +static int +dwb_cycle_internal(int npages) +{ + DWBSlotRef refs[DWB_BATCH_MAX_PAGES]; + int nrefs = 0; + int last_batch = -1; + int retired = 0; + static char page[BLCKSZ]; + + for (int i = 0; i < npages; i++) + { + BufferTag tag; + DWBSlotRef ref; + RelFileLocator rlocator; + + CHECK_FOR_INTERRUPTS(); + + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = 1; + rlocator.relNumber = 90000 + (i % 3); + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, (BlockNumber) i); + + DWBAcquireSlot(&tag, &ref); + + /* + * A batch switch means the previous batch overflowed and was sealed + * by its overflow writer: its refs can be waited for and dropped. + */ + if (last_batch >= 0 && ref.batch_idx != last_batch) + { + wait_and_release(refs, nrefs); + nrefs = 0; + retired += DWBRetireAllSync(); + } + last_batch = ref.batch_idx; + + memset(page, 'A' + (i % 26), BLCKSZ); + DWBPublishImage(&ref, page, (XLogRecPtr) 0x1000000 + i); + refs[nrefs++] = ref; + } + + /* seal the tail batch and drain */ + DWBForceSealOpenBatch(DWB_WCLASS_EVICTION); + wait_and_release(refs, nrefs); + retired += DWBRetireAllSync(); + return retired; +} + +PG_FUNCTION_INFO_V1(test_dwb_cycle); +Datum +test_dwb_cycle(PG_FUNCTION_ARGS) +{ + int npages = PG_GETARG_INT32(0); + + check_dwb_enabled(); + if (npages < 1 || npages > 100000) + ereport(ERROR, (errmsg("npages out of range"))); + + PG_RETURN_INT32(dwb_cycle_internal(npages)); +} + +PG_FUNCTION_INFO_V1(test_dwb_stress); +Datum +test_dwb_stress(PG_FUNCTION_ARGS) +{ + int loops = PG_GETARG_INT32(0); + int npages = PG_GETARG_INT32(1); + + check_dwb_enabled(); + for (int i = 0; i < loops; i++) + { + CHECK_FOR_INTERRUPTS(); + (void) dwb_cycle_internal(npages); + } + PG_RETURN_VOID(); +} + +/* + * Validate the on-disk ring the way the apply-pass will: read every batch + * file, check header, then count slots passing meta_crc (+ generation if + * current_only) + flags + image_crc. + */ +PG_FUNCTION_INFO_V1(test_dwb_ring_slots); +Datum +test_dwb_ring_slots(PG_FUNCTION_ARGS) +{ + bool current_only = PG_GETARG_BOOL(0); + DWBControlFileData control; + Size meta_region; + DWSlotMeta *metas; + char *image; + int valid = 0; + + check_dwb_enabled(); + if (!DWBReadControlFile(&control, false)) + pg_unreachable(); + + meta_region = DWBMetaRegionSize(control.batch_pages); + metas = palloc(control.batch_pages * sizeof(DWSlotMeta)); + image = palloc(BLCKSZ); + + for (uint32 b = 0; b < control.num_batches; b++) + { + char path[MAXPGPATH]; + int fd; + DWBBatchHeader hdr; + + snprintf(path, MAXPGPATH, DWB_DIR "/batch_%04u", b); + fd = OpenTransientFile(path, O_RDONLY | PG_BINARY); + if (fd < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", path))); + + if (pg_pread(fd, &hdr, sizeof(hdr), 0) != sizeof(hdr)) + ereport(ERROR, (errmsg("short read of header in \"%s\"", path))); + + /* an all-zero (never written) batch fails the header check */ + if (hdr.magic == DWB_BATCH_MAGIC && + EQ_CRC32C(hdr.crc, DWBBatchHeaderCrc(&hdr)) && + hdr.n_slots <= control.batch_pages) + { + int nbytes = hdr.n_slots * sizeof(DWSlotMeta); + + if (pg_pread(fd, metas, nbytes, sizeof(DWBBatchHeader)) != nbytes) + ereport(ERROR, (errmsg("short read of metas in \"%s\"", path))); + + for (uint32 i = 0; i < hdr.n_slots; i++) + { + DWSlotMeta *meta = &metas[i]; + + if (!EQ_CRC32C(meta->meta_crc, DWBSlotMetaCrc(meta))) + continue; + if (meta->flags & DWB_SLOT_ABORTED) + continue; + if (current_only && meta->generation != control.generation) + continue; + + if (pg_pread(fd, image, BLCKSZ, + meta_region + (off_t) i * BLCKSZ) != BLCKSZ) + ereport(ERROR, (errmsg("short read of image in \"%s\"", path))); + if (!EQ_CRC32C(meta->image_crc, DWBImageCrc(image))) + continue; + valid++; + } + } + + if (CloseTransientFile(fd) != 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", path))); + } + + pfree(metas); + pfree(image); + PG_RETURN_INT32(valid); +} + +PG_FUNCTION_INFO_V1(test_dwb_states); +Datum +test_dwb_states(PG_FUNCTION_ARGS) +{ + int counts[DWB_OOM_RETIRING + 1] = {0}; + StringInfoData buf; + + check_dwb_enabled(); + for (int i = 0; i < dwb_num_batches; i++) + { + DWBatchState state = DWBGetBatchState(i); + + if (state <= DWB_OOM_RETIRING) + counts[state]++; + } + + initStringInfo(&buf); + appendStringInfo(&buf, + "free=%d allocated=%d sealed=%d written=%d fsynced=%d data_written=%d retiring=%d", + counts[DWB_FREE], counts[DWB_ALLOCATED], + counts[DWB_SEALED], counts[DWB_WRITTEN], + counts[DWB_FSYNCED], counts[DWB_DATA_WRITTEN], + counts[DWB_RETIRING] + counts[DWB_OOM_RETIRING]); + PG_RETURN_TEXT_P(cstring_to_text(buf.data)); +} diff --git a/src/test/modules/test_dwb/test_dwb.conf b/src/test/modules/test_dwb/test_dwb.conf new file mode 100644 index 0000000000000..ff20a0a481f18 --- /dev/null +++ b/src/test/modules/test_dwb/test_dwb.conf @@ -0,0 +1,3 @@ +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 diff --git a/src/test/modules/test_dwb/test_dwb.control b/src/test/modules/test_dwb/test_dwb.control new file mode 100644 index 0000000000000..3063bd3cc54c8 --- /dev/null +++ b/src/test/modules/test_dwb/test_dwb.control @@ -0,0 +1,4 @@ +comment = 'Test code for the short-lived double write buffer' +default_version = '1.0' +module_pathname = '$libdir/test_dwb' +relocatable = true From 8d75a77e1a54e8570dde95d8ec6b76e759ded99f Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 24 Jul 2026 08:41:51 +0300 Subject: [PATCH 02/52] Harden the DWB skeleton after review (Stage 1 follow-up) Make the leader write crash-consistent and close the liveness holes found in review: * Run the leader span (seal win -> DWB_FSYNCED broadcast) as a critical section: an I/O error there cannot be unwound - the seal winner is the only process able to advance the batch - so it now escalates to PANIC instead of leaving the batch, its waiters and its staging buffer wedged forever. Everything that may fail harmlessly (the one-time leader allocations, the batch file VFD, the backend's condition- variable wait event set) moves into a pre-seal prepare phase, where an ERROR leaves the batch ALLOCATED for another writer to seal later. * Pin the batch with a leader ref from SEAL to FSYNCED: a sealed batch can no longer drop to zero refs before it is durable, so the FSYNCED -> RETIRING hand-off always has exactly one owner, even when every writer exits during the write. * Guard the seg_set dedup scan with the batch's publish_lock LWLock instead of a spinlock (the O(n) scan far exceeds the spinlock hold-time rule); drop the seg_lock field. * Reserve the staging buffer before taking DWBRingOpenLock, leaving no sleeping or interruptible point while a batch is held out of DWB_FREE. * Assorted hardening: ConditionVariableCancelSleep after wait loops, a hard ERROR on pendingRefs overflow, batch_id assertions in the writer-side API, uint16 slot_flags to match the on-disk width, StaticAssertDecl pins on the on-disk struct layout, canonical read-error reporting, ssize_t for FileWrite results, a symbolic GUC maximum for dwb_batch_pages, and comments aligned with the Stage 1 reality. * test_dwb: prove CRC rejection by flipping bytes in a batch file, exercise both DWBProcExit paths with a backend dying mid-batch (the orphan scenario is a direct regression test for the leader pin), refuse-start coverage for dwb_batch_pages, and tighter bounds. --- src/backend/access/transam/xlog.c | 6 +- src/backend/storage/dwb/dwb.c | 139 ++++++++++++++++---- src/backend/storage/dwb/dwb_ctl.c | 1 - src/backend/storage/dwb/dwb_file.c | 57 ++++++-- src/backend/utils/misc/guc_tables.c | 10 +- src/include/storage/dwb.h | 50 +++++-- src/test/modules/test_dwb/t/001_dwb.pl | 89 ++++++++++++- src/test/modules/test_dwb/test_dwb--1.0.sql | 12 ++ src/test/modules/test_dwb/test_dwb.c | 125 ++++++++++++++++-- 9 files changed, 423 insertions(+), 66 deletions(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index f681164ea28ca..3e1c57825c580 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -5597,9 +5597,9 @@ StartupXLOG(void) /* * Create or validate the double write buffer ring and durably bump its - * generation before any of its slots can be written or applied. The - * apply-pass over the previous generation runs here, before WAL - * recovery is initialized. + * generation before any of its slots can be written. (The Stage 4 + * apply-pass over the previous generation will run here, before WAL + * recovery is initialized.) */ DWBStartup(); diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index b94e5a8a1a5de..ac2a2652e7bcd 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -9,8 +9,9 @@ * their page image with a plain memcpy into the batch's staging buffer and * set their bit in slots_written_bitmap. The SEAL initiator becomes the * leader: it waits for bitmap coverage of capped_slots, then writes the - * whole batch (contiguous image stream, then the meta region, then - * fdatasync) and broadcasts DWB_FSYNCED. + * whole batch — in this write order: the contiguous image stream, then the + * meta region, then fdatasync (the on-disk layout puts the meta region + * first; see dwb.h) — and broadcasts DWB_FSYNCED. * * Stage 1 scope: the state machine is complete but not yet wired into * FlushBuffer; retirement is synchronous (DWBRetireAllSync) — the retire @@ -37,14 +38,19 @@ /* * Slot refs held by this backend, for cleanup on process exit. A ref lives - * from DWBAcquireSlot to DWBReleaseSlot. (Stage 2 additionally attaches - * refs to the ResourceOwner so that a transaction abort — e.g. an ERROR out - * of smgrwrite — releases them too.) + * from DWBAcquireSlot to DWBReleaseSlot. Sized to two full batches because + * a backend can hold refs on a sealed batch and on its successor at the + * same time, releasing the former only after the switch. (Stage 2 + * additionally attaches refs to the ResourceOwner so that a transaction + * abort — e.g. an ERROR out of smgrwrite — releases them too.) */ static DWBSlotRef pendingRefs[2 * DWB_BATCH_MAX_PAGES]; static int nPendingRefs = 0; static bool cleanup_registered = false; +/* leader-side meta assembly area, allocated before the seal is attempted */ +static DWSlotMeta *leader_metas = NULL; + static void DWBProcExit(int code, Datum arg); static void DWBLeaderWriteBatch(int batch_idx); static void DWBFinishBatchData(DWBatchCtl *batch); @@ -64,9 +70,11 @@ DWBStagingSlotPtr(int staging_idx, int slot_idx) static int DWBStagingAlloc(void) { + int idx; + for (;;) { - int idx = -1; + idx = -1; SpinLockAcquire(&DWBCtl->staging_lock); if (DWBCtl->staging_free != 0) @@ -77,12 +85,18 @@ DWBStagingAlloc(void) SpinLockRelease(&DWBCtl->staging_lock); if (idx >= 0) - return idx; + break; - /* released together with retired batches / after leader writes */ + /* + * A buffer frees once its leader finishes the image pwrite; + * retirement broadcasts cv_free_batch too, so just re-check on + * every wake-up. + */ ConditionVariableSleep(&DWBCtl->cv_free_batch, WAIT_EVENT_DWB_FREE_BATCH); } + ConditionVariableCancelSleep(); + return idx; } static void @@ -116,6 +130,15 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) for (;;) { int free_idx = -1; + int staging_idx; + + /* + * Reserve the staging buffer before taking the lock: the wait for a + * free buffer can be long, and no sleeping (or interruptible) point + * may exist below, where we hold DWBRingOpenLock with a batch + * already taken out of DWB_FREE. + */ + staging_idx = DWBStagingAlloc(); LWLockAcquire(DWBRingOpenLock, LW_EXCLUSIVE); @@ -123,6 +146,8 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) if (pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]) != old_idx) { LWLockRelease(DWBRingOpenLock); + DWBStagingRelease(staging_idx); + ConditionVariableCancelSleep(); return; } @@ -152,7 +177,7 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) batch->max_page_lsn = InvalidXLogRecPtr; batch->batch_id = pg_atomic_fetch_add_u64(&DWBCtl->next_batch_id, 1); batch->open_time = GetCurrentTimestamp(); - batch->staging_idx = DWBStagingAlloc(); + batch->staging_idx = staging_idx; /* * Open for reservations only after everything above is visible: @@ -163,10 +188,12 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) pg_atomic_write_u32(&DWBCtl->open_batch_idx[wclass], free_idx); LWLockRelease(DWBRingOpenLock); + ConditionVariableCancelSleep(); return; } LWLockRelease(DWBRingOpenLock); + DWBStagingRelease(staging_idx); /* whole ring busy: wait for a retirement, then retry */ ConditionVariableSleep(&DWBCtl->cv_free_batch, @@ -192,10 +219,37 @@ DWBSealBatch(int batch_idx) uint32 capped; uint32 expected; + /* + * Get everything the critical section below could fail at out of the + * way while failure is still harmless (the seal has not been attempted + * yet, so on ERROR the batch stays ALLOCATED and any other writer can + * seal it later): the one-time leader allocations, the batch file VFD, + * and this backend's condition-variable wait event set (the first + * ConditionVariablePrepareToSleep of a backend allocates it). + */ + if (leader_metas == NULL) + leader_metas = MemoryContextAllocZero(TopMemoryContext, + DWB_BATCH_MAX_PAGES * sizeof(DWSlotMeta)); + DWBPrepareBatchWrite(batch_idx); + ConditionVariablePrepareToSleep(&batch->cv_state); + ConditionVariableCancelSleep(); + prev = pg_atomic_fetch_or_u32(&batch->next_slot_idx, DWB_SEAL_BIT); if (prev & DWB_SEAL_BIT) return false; /* somebody else is the leader */ + /* + * We are the leader: nobody else can advance this batch anymore. A + * failure between here and DWB_FSYNCED would leave the batch wedged + * forever, its waiters stuck and its staging buffer lost, so run the + * whole span as a critical section: any error escalates to PANIC and + * crash recovery resets the ring. (This also suspends interrupt + * processing, making the coverage wait in DWBLeaderWriteBatch + * non-interruptible; its dwb_slot_stuck_timeout_ms PANIC is the + * backstop.) + */ + START_CRIT_SECTION(); + capped = Min(prev & DWB_IDX_MASK, (uint32) dwb_batch_pages); pg_atomic_write_u32(&batch->capped_slots, capped); pg_write_barrier(); @@ -216,27 +270,49 @@ DWBSealBatch(int batch_idx) batch->staging_idx = -1; pg_atomic_write_u32(&batch->state, DWB_FREE); ConditionVariableBroadcast(&DWBCtl->cv_free_batch); + END_CRIT_SECTION(); return true; } + /* + * Pin the batch with a leader ref for the duration of the write. All + * writers may exit while we write (dropping their refs), and the + * FSYNCED -> RETIRING hand-off runs when the last ref drops: the pin + * guarantees ref_count stays above zero until DWB_FSYNCED is reached, + * so the hand-off always has exactly one well-defined owner. + */ + pg_atomic_fetch_add_u32(&batch->ref_count, 1); + DWBLeaderWriteBatch(batch_idx); + + END_CRIT_SECTION(); + + if (pg_atomic_fetch_sub_u32(&batch->ref_count, 1) == 1) + DWBFinishBatchData(batch); + return true; } /* * Leader: wait for bitmap coverage of capped_slots, write the batch, * fdatasync, broadcast DWB_FSYNCED. + * + * Runs inside the leader's critical section (see DWBSealBatch); everything + * it needs was allocated and opened before the seal, so no palloc happens + * here. */ static void DWBLeaderWriteBatch(int batch_idx) { DWBatchCtl *batch = &DWBCtl->batches[batch_idx]; uint32 capped = pg_atomic_read_u32(&batch->capped_slots); - static DWSlotMeta *metas = NULL; DWBBatchHeader hdr; TimestampTz wait_start = GetCurrentTimestamp(); uint32 expected; + Assert(CritSectionCount > 0); + Assert(leader_metas != NULL); + /* * Coverage wait is memcpy-bound: writers do no I/O between reserving a * slot and setting their bit. The timeout is a defensive backstop @@ -281,14 +357,11 @@ DWBLeaderWriteBatch(int batch_idx) batch_idx, expected); /* assemble slot metas entirely from shmem arrays */ - if (metas == NULL) - metas = MemoryContextAllocZero(TopMemoryContext, - DWB_BATCH_MAX_PAGES * sizeof(DWSlotMeta)); - memset(metas, 0, capped * sizeof(DWSlotMeta)); + memset(leader_metas, 0, capped * sizeof(DWSlotMeta)); batch->max_page_lsn = InvalidXLogRecPtr; for (uint32 i = 0; i < capped; i++) { - DWSlotMeta *meta = &metas[i]; + DWSlotMeta *meta = &leader_metas[i]; meta->tag = batch->pages[i]; meta->page_lsn = batch->page_lsns[i]; @@ -307,7 +380,7 @@ DWBLeaderWriteBatch(int batch_idx) hdr.n_slots = capped; hdr.crc = DWBBatchHeaderCrc(&hdr); - DWBWriteBatch(batch_idx, &hdr, metas, + DWBWriteBatch(batch_idx, &hdr, leader_metas, DWBStagingSlotPtr(batch->staging_idx, 0)); /* image pwrite done — staging can serve the next batch */ @@ -336,7 +409,10 @@ DWBAcquireSlot(const BufferTag *tag, DWBSlotRef *ref) const int wclass = DWB_WCLASS_EVICTION; Assert(DWBIsEnabled()); - Assert(nPendingRefs < 2 * DWB_BATCH_MAX_PAGES); + + /* hard bound: overflowing the static array would corrupt memory */ + if (nPendingRefs >= (int) lengthof(pendingRefs)) + elog(ERROR, "too many pending double write buffer slot refs held by one backend"); if (!cleanup_registered) { @@ -388,7 +464,11 @@ DWBAcquireSlot(const BufferTag *tag, DWBSlotRef *ref) seg.forknum = BufTagGetForkNum(tag); seg.segno = tag->blockNum / RELSEG_SIZE; - SpinLockAcquire(&batch->seg_lock); + /* + * The dedup scan is O(n_segs), far too long for a spinlock; + * publish_lock is this batch's LWLock over n_segs/seg_set. + */ + LWLockAcquire(&batch->publish_lock, LW_EXCLUSIVE); for (uint32 i = 0; i < batch->n_segs; i++) { if (RelFileLocatorEquals(batch->seg_set[i].rlocator, seg.rlocator) && @@ -401,7 +481,7 @@ DWBAcquireSlot(const BufferTag *tag, DWBSlotRef *ref) } if (!found) batch->seg_set[batch->n_segs++] = seg; - SpinLockRelease(&batch->seg_lock); + LWLockRelease(&batch->publish_lock); } pg_atomic_fetch_add_u32(&batch->ref_count, 1); @@ -423,6 +503,9 @@ DWBPublishImage(const DWBSlotRef *ref, const char *image, XLogRecPtr page_lsn) { DWBatchCtl *batch = &DWBCtl->batches[ref->batch_idx]; + /* a held ref pins the batch, so its incarnation cannot have changed */ + Assert(ref->batch_id == batch->batch_id); + memcpy(DWBStagingSlotPtr(batch->staging_idx, ref->slot_idx), image, BLCKSZ); batch->page_lsns[ref->slot_idx] = page_lsn; @@ -443,6 +526,8 @@ DWBWaitBatchFsynced(const DWBSlotRef *ref) { DWBatchCtl *batch = &DWBCtl->batches[ref->batch_idx]; + Assert(ref->batch_id == batch->batch_id); + ConditionVariablePrepareToSleep(&batch->cv_state); while (pg_atomic_read_u32(&batch->state) < DWB_FSYNCED) ConditionVariableSleep(&batch->cv_state, WAIT_EVENT_DWB_BATCH_FSYNC); @@ -450,9 +535,10 @@ DWBWaitBatchFsynced(const DWBSlotRef *ref) } /* - * Step 7 of the write path: the last ref publishes the segment set and - * moves the batch to RETIRING. (Stage 2 publishes into DWSegmentHash here; - * Stage 1 retirement is DWBRetireAllSync.) + * Step 7 of the write path: the last ref hands the batch over to + * retirement (FSYNCED -> DATA_WRITTEN -> RETIRING). (Stage 2 publishes + * the segment set into DWSegmentHash here; Stage 1 retirement is + * DWBRetireAllSync.) */ static void DWBFinishBatchData(DWBatchCtl *batch) @@ -482,6 +568,8 @@ DWBReleaseSlot(const DWBSlotRef *ref) { DWBatchCtl *batch = &DWBCtl->batches[ref->batch_idx]; + Assert(ref->batch_id == batch->batch_id); + for (int i = 0; i < nPendingRefs; i++) { if (pendingRefs[i].batch_idx == ref->batch_idx && @@ -592,6 +680,13 @@ DWBProcExit(int code, Datum arg) batch->orphan_tags[n] = batch->pages[ref.slot_idx]; } + /* + * The last ref finishes the batch only once it is FSYNCED. A + * sealed batch cannot lose its last ref earlier — the leader holds + * its own pin from SEAL to FSYNCED (see DWBSealBatch) — so reaching + * zero refs in an earlier state means the batch is not sealed yet: + * it stays open and a later seal completes it normally. + */ if (pg_atomic_fetch_sub_u32(&batch->ref_count, 1) == 1 && pg_atomic_read_u32(&batch->state) == DWB_FSYNCED) DWBFinishBatchData(batch); diff --git a/src/backend/storage/dwb/dwb_ctl.c b/src/backend/storage/dwb/dwb_ctl.c index fc75657c1343e..d9dfb8e63e760 100644 --- a/src/backend/storage/dwb/dwb_ctl.c +++ b/src/backend/storage/dwb/dwb_ctl.c @@ -97,7 +97,6 @@ DWBShmemInit(void) pg_atomic_init_u32(&batch->orphaned_refs_count, 0); LWLockInitialize(&batch->publish_lock, LWTRANCHE_DWB_PUBLISH); ConditionVariableInit(&batch->cv_state); - SpinLockInit(&batch->seg_lock); batch->staging_idx = -1; } } diff --git a/src/backend/storage/dwb/dwb_file.c b/src/backend/storage/dwb/dwb_file.c index 3025642b609f8..aa1d5c34d4a28 100644 --- a/src/backend/storage/dwb/dwb_file.c +++ b/src/backend/storage/dwb/dwb_file.c @@ -32,6 +32,9 @@ * workers ever open batch files */ static File *batch_files = NULL; +/* IO-aligned meta-region assembly buffer, allocated by DWBPrepareBatchWrite */ +static char *meta_buf = NULL; + pg_crc32c DWBImageCrc(const char *image) { @@ -104,13 +107,22 @@ DWBReadControlFile(DWBControlFileData *control, bool missing_ok) } pgstat_report_wait_start(WAIT_EVENT_DWB_CONTROL_READ); + errno = 0; r = read(fd, control, sizeof(DWBControlFileData)); pgstat_report_wait_end(); if (r != sizeof(DWBControlFileData)) + { + /* distinguish a real read error from a truncated file */ + if (r < 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", + DWB_CONTROL_FILE))); ereport(FATAL, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("could not read file \"%s\": read %d of %zu", DWB_CONTROL_FILE, r, sizeof(DWBControlFileData)))); + } if (CloseTransientFile(fd) != 0) ereport(FATAL, (errcode_for_file_access(), @@ -278,30 +290,49 @@ DWBCloseBatchFiles(void) } } +/* + * Pre-open the batch file and pre-allocate the meta-region buffer, so that + * DWBWriteBatch can run inside the leader's critical section without + * allocating anything. Called before the seal is attempted, where an + * ERROR is still harmless. + */ +void +DWBPrepareBatchWrite(int batch_idx) +{ + if (meta_buf == NULL) + meta_buf = MemoryContextAllocAligned(TopMemoryContext, + DWBMetaRegionSize(dwb_batch_pages), + PG_IO_ALIGN_SIZE, 0); + (void) DWBOpenBatchFile(batch_idx); +} + /* * Leader write of one batch: (a) one contiguous pwrite of the image stream * from staging, (b) one pwrite of the meta region, (c) fdatasync. Exactly * this order: a crash while reusing a slot must never leave valid-looking * meta over a torn or foreign image (any partially-persistent mix is * rejected locally by meta_crc/generation/image_crc, see 3.2/3.4). + * + * Runs inside the leader's critical section: every ereport here escalates + * to PANIC, which is deliberate — an incomplete leader write cannot be + * unwound (see DWBSealBatch). */ void DWBWriteBatch(int batch_idx, const DWBBatchHeader *hdr, const DWSlotMeta *metas, const char *images) { - static char *meta_buf = NULL; Size meta_region = DWBMetaRegionSize(dwb_batch_pages); File file = DWBOpenBatchFile(batch_idx); Size image_bytes = (Size) hdr->n_slots * BLCKSZ; - int rc; + ssize_t nwritten; + int fd; - if (meta_buf == NULL) - meta_buf = MemoryContextAllocAligned(TopMemoryContext, meta_region, - PG_IO_ALIGN_SIZE, 0); + /* DWBPrepareBatchWrite has run */ + Assert(meta_buf != NULL); - rc = FileWrite(file, images, image_bytes, meta_region, - WAIT_EVENT_DWB_BATCH_WRITE); - if (rc != image_bytes) + nwritten = FileWrite(file, images, image_bytes, meta_region, + WAIT_EVENT_DWB_BATCH_WRITE); + if (nwritten != (ssize_t) image_bytes) ereport(ERROR, (errcode_for_file_access(), errmsg("could not write batch %d of \"%s\": %m", @@ -312,9 +343,9 @@ DWBWriteBatch(int batch_idx, const DWBBatchHeader *hdr, memcpy(meta_buf + sizeof(DWBBatchHeader), metas, hdr->n_slots * sizeof(DWSlotMeta)); - rc = FileWrite(file, meta_buf, meta_region, 0, - WAIT_EVENT_DWB_BATCH_WRITE); - if (rc != meta_region) + nwritten = FileWrite(file, meta_buf, meta_region, 0, + WAIT_EVENT_DWB_BATCH_WRITE); + if (nwritten != (ssize_t) meta_region) ereport(ERROR, (errcode_for_file_access(), errmsg("could not write batch %d of \"%s\": %m", @@ -325,8 +356,8 @@ DWBWriteBatch(int batch_idx, const DWBBatchHeader *hdr, * its size and block layout never change (WAL-segment contract). */ pgstat_report_wait_start(WAIT_EVENT_DWB_BATCH_SYNC); - rc = FileGetRawDesc(file); - if (rc < 0 || pg_fdatasync(rc) != 0) + fd = FileGetRawDesc(file); + if (fd < 0 || pg_fdatasync(fd) != 0) ereport(data_sync_elevel(ERROR), (errcode_for_file_access(), errmsg("could not fsync batch %d of \"%s\": %m", diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index e93e0fd4ac082..e7c37043799df 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -356,10 +356,6 @@ static const struct config_enum_entry synchronous_commit_options[] = { {NULL, 0, false} }; -/* - * Although only "on", "off", "try" are documented, we accept all the likely - * variants of "on" and "off". - */ static const struct config_enum_entry io_torn_pages_protection_options[] = { {"off", DWB_PROTECT_OFF, false}, {"full_pages", DWB_PROTECT_FULL_PAGES, false}, @@ -374,6 +370,10 @@ static const struct config_enum_entry dwb_on_stall_options[] = { {NULL, 0, false} }; +/* + * Although only "on", "off", "try" are documented, we accept all the likely + * variants of "on" and "off". + */ static const struct config_enum_entry huge_pages_options[] = { {"off", HUGE_PAGES_OFF, false}, {"on", HUGE_PAGES_ON, false}, @@ -2211,7 +2211,7 @@ struct config_int ConfigureNamesInt[] = NULL }, &dwb_batch_pages, - 64, 16, 256, + 64, 16, DWB_BATCH_MAX_PAGES, NULL, NULL, NULL }, { diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 79b1bb5259ed1..85e73b8170651 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -85,7 +85,8 @@ extern PGDLLIMPORT int dwb_on_stall; * pg_dwb/control - geometry + durable generation, written atomically * pg_dwb/batch_NNNN - meta region (header + slot metas, padded to * PG_IO_ALIGN_SIZE) followed by a contiguous - * BLCKSZ-aligned page-image stream + * PG_IO_ALIGN_SIZE-aligned stream of BLCKSZ page + * images * * Slot validity is locally verifiable: meta_crc rejects a torn meta write * (including any old/new field mix on slot reuse), image_crc rejects a torn @@ -130,6 +131,23 @@ typedef struct DWSlotMeta /* DWSlotMeta.flags */ #define DWB_SLOT_ABORTED 0x0001 /* writer died before publishing */ +/* + * The on-disk layout is pinned: any change to these sizes or offsets is an + * on-disk format change and requires a DWB_VERSION bump. + */ +StaticAssertDecl(sizeof(DWBControlFileData) == 40, + "DWBControlFileData on-disk size changed"); +StaticAssertDecl(offsetof(DWBControlFileData, crc) == 32, + "DWBControlFileData crc offset changed"); +StaticAssertDecl(sizeof(DWBBatchHeader) == 24, + "DWBBatchHeader on-disk size changed"); +StaticAssertDecl(offsetof(DWBBatchHeader, crc) == 20, + "DWBBatchHeader crc offset changed"); +StaticAssertDecl(sizeof(DWSlotMeta) == 56, + "DWSlotMeta on-disk size changed"); +StaticAssertDecl(offsetof(DWSlotMeta, meta_crc) == 48, + "DWSlotMeta meta_crc offset changed"); + #define DWBMetaRegionSize(batch_pages) \ TYPEALIGN(PG_IO_ALIGN_SIZE, \ sizeof(DWBBatchHeader) + (batch_pages) * sizeof(DWSlotMeta)) @@ -138,6 +156,11 @@ typedef struct DWSlotMeta /* * Batch lifecycle. A slot is reused only via DWB_FREE. + * + * The numeric order of the happy-path states is semantic: the code uses + * comparisons like "state < DWB_FSYNCED" as progress tests. Insert new + * states only in lifecycle order; DWB_OOM_RETIRING is a side fork of + * DWB_RETIRING and must stay numerically last. */ typedef enum DWBatchState { @@ -149,7 +172,8 @@ typedef enum DWBatchState DWB_FSYNCED, /* batch durable; writers do smgrwrite */ DWB_DATA_WRITTEN, /* all smgrwrite + sync requests done */ DWB_RETIRING, /* waiting for fsync of seg_set segments */ - DWB_OOM_RETIRING, /* publisher retires synchronously (3.5) */ + DWB_OOM_RETIRING, /* publisher retires synchronously (Stage 2, + * 3.5) */ } DWBatchState; typedef struct DWSegRef @@ -174,22 +198,28 @@ typedef struct DWBatchCtl pg_atomic_uint64 slots_written_bitmap[DWB_BITMAP_WORDS]; pg_atomic_uint32 ref_count; /* writers holding the batch from slot * reservation to smgrwrite done */ - pg_atomic_uint32 seg_pending_count; /* seg_set entries not yet fsynced */ + pg_atomic_uint32 seg_pending_count; /* seg_set entries not yet fsynced; + * Stage 1 sets and clears it + * wholesale, Stage 2 decrements it + * per fsynced segment */ pg_atomic_uint32 orphaned_refs_count; /* refs whose writer aborted after * publishing the copy but before * smgrwrite; a retire worker - * finishes their writes */ - LWLock publish_lock; /* serializes seg_set publication and - * seg_pending_count decrement (3.5) */ + * finishes their writes (Stage 2; + * Stage 1 only records them) */ + LWLock publish_lock; /* protects n_segs/seg_set (dedup insert); + * Stage 2 also serializes seg_set + * publication and the seg_pending_count + * decrement (3.5) */ ConditionVariable cv_state; /* broadcast on state change */ - slock_t seg_lock; /* protects n_segs/seg_set dedup insert */ uint32 n_segs; DWSegRef seg_set[DWB_BATCH_MAX_SEGS]; BufferTag pages[DWB_BATCH_MAX_PAGES]; XLogRecPtr page_lsns[DWB_BATCH_MAX_PAGES]; pg_crc32c image_crcs[DWB_BATCH_MAX_PAGES]; /* computed by writers at * publication */ - uint8 slot_flags[DWB_BATCH_MAX_PAGES]; + uint16 slot_flags[DWB_BATCH_MAX_PAGES]; /* same width as + * DWSlotMeta.flags */ int staging_idx; /* staging buffer; held from ALLOCATED until * the leader finishes the image pwrite */ BufferTag orphan_tags[DWB_BATCH_MAX_PAGES]; @@ -210,7 +240,8 @@ typedef struct DWCtl * bump; constant until restart, stamped * into DWSlotMeta by the leader */ ConditionVariable cv_free_batch; /* broadcast on retire */ - ConditionVariable cv_retire_wake; /* wakes retire workers */ + ConditionVariable cv_retire_wake; /* wakes retire workers (Stage 2; no + * waiters yet) */ slock_t staging_lock; /* protects staging_free bitmap */ uint32 staging_free; /* bitmap of free staging buffers */ DWBatchCtl batches[FLEXIBLE_ARRAY_MEMBER]; /* dwb_num_batches entries */ @@ -249,6 +280,7 @@ extern bool DWBReadControlFile(DWBControlFileData *control, bool missing_ok); extern void DWBWriteControlFile(const DWBControlFileData *control); extern int DWBOpenBatchFile(int batch_idx); extern void DWBCloseBatchFiles(void); +extern void DWBPrepareBatchWrite(int batch_idx); extern void DWBWriteBatch(int batch_idx, const DWBBatchHeader *hdr, const DWSlotMeta *metas, const char *images); extern pg_crc32c DWBImageCrc(const char *image); diff --git a/src/test/modules/test_dwb/t/001_dwb.pl b/src/test/modules/test_dwb/t/001_dwb.pl index b227fd35de00c..83766e7e1eb02 100644 --- a/src/test/modules/test_dwb/t/001_dwb.pl +++ b/src/test/modules/test_dwb/t/001_dwb.pl @@ -30,6 +30,35 @@ is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), '24', 'all surviving slots validate (meta_crc, generation, image_crc)'); +# --- torn copies are rejected by the on-disk CRCs ---------------------- + +# Batch file layout with dwb_batch_pages = 16: a 24-byte header, 16 slot +# metas of 56 bytes each (sizes pinned by StaticAssertDecl in dwb.h), the +# meta region padded to 4096; page images follow at 4096 + slot * 8192. +my $bfile = $node->data_dir . '/pg_dwb/batch_0001'; + +sub flip_byte +{ + my ($file, $offset) = @_; + open my $bf, '+<:raw', $file or die "open $file: $!"; + sysseek($bf, $offset, 0) // die "seek: $!"; + die "read: $!" unless sysread($bf, my $byte, 1) == 1; + sysseek($bf, $offset, 0) // die "seek: $!"; + die "write: $!" unless syswrite($bf, chr(ord($byte) ^ 0xFF), 1) == 1; + close $bf; + return; +} + +# a torn image: one flipped byte inside slot 0's page image +flip_byte($bfile, 4096 + 100); +is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), + '23', 'image_crc rejects a torn page image'); + +# a torn meta: one flipped byte inside slot 1's meta (offset 24 + 56) +flip_byte($bfile, 24 + 56); +is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), + '22', 'meta_crc rejects a torn slot meta'); + # --- concurrent writers over a small ring ------------------------------ # 3 clients x 30 transactions x 40 pages through a 16x16 ring @@ -45,18 +74,62 @@ $node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=16 allocated=0 sealed=0 written=0 fsynced=0 data_written=0 retiring=0/, 'ring fully retired after concurrent stress'); -cmp_ok($node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), - '>', 0, 'ring holds valid current-generation slots after stress'); +my $valid = $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'); +cmp_ok($valid, '>', 0, 'ring holds valid current-generation slots after stress'); +cmp_ok($valid, '<=', 16 * 16, 'slot count bounded by the ring capacity'); # --- restart bumps the durable generation ------------------------------ my $stale = $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(false)'); +cmp_ok($stale, '>', 0, 'ring holds slots before the restart check'); $node->restart; is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), '0', 'no slot belongs to the new generation after restart'); is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(false)'), $stale, 'stale slots still CRC-valid, only the generation gates them'); +# --- process exit cleanup ---------------------------------------------- + +# A backend dies holding unpublished slots: DWBProcExit poisons them, the +# next seal's coverage wait is satisfied by the poison bits, the batch +# completes and the aborted slots never validate. +my $bg = $node->background_psql('postgres'); +$bg->query_safe('SELECT test_dwb_leak(3, false)'); +$bg->quit; +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/allocated=1/, 'abandoned batch stays open'); +is( $node->safe_psql('postgres', 'SELECT test_dwb_force_seal()'), + 't', 'abandoned batch seals'); +$node->poll_query_until('postgres', + "SELECT test_dwb_states() LIKE '%retiring=1%'") + or die 'timed out waiting for the abandoned batch to reach RETIRING'; +is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), + '1', 'abandoned batch retires'); +is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), + '0', 'aborted slots are not apply candidates'); + +# A backend dies after publishing: the copies are still written out and +# validate; the leader's own pin hands the batch over to retirement even +# though no writer is left alive. +$bg = $node->background_psql('postgres'); +$bg->query_safe('SELECT test_dwb_leak(3, true)'); +$bg->quit; +is( $node->safe_psql('postgres', 'SELECT test_dwb_force_seal()'), + 't', 'orphaned batch seals'); +# the dead backend's ProcExit may still be releasing its refs: wait for +# the FSYNCED -> RETIRING hand-off instead of assuming it already happened +$node->poll_query_until('postgres', + "SELECT test_dwb_states() LIKE '%retiring=1%'") + or die 'timed out waiting for the orphaned batch to reach RETIRING'; +is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), + '1', 'orphaned batch retires'); +is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), + '3', 'published slots of a dead backend survive and validate'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/free=16/, 'ring fully idle after the orphan hand-off'); + # --- geometry is fixed by the on-disk control file --------------------- $node->stop; @@ -69,6 +142,18 @@ $node->start; $node->stop; +# the second geometry GUC is enforced independently +my $log_offset = -s $node->logfile; +$node->append_conf('postgresql.conf', 'dwb_batch_pages = 32'); +$ret = $node->start(fail_ok => 1); +is($ret, 0, 'start refused after batch_pages change'); +ok( $node->log_contains('was created with dwb_num_batches = 16 and dwb_batch_pages = 16', + $log_offset), + 'batch_pages mismatch reported'); +$node->append_conf('postgresql.conf', 'dwb_batch_pages = 16'); +$node->start; +$node->stop; + # --- double_writes requires data checksums ----------------------------- my $node2 = PostgreSQL::Test::Cluster->new('dwb_nochecksums'); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index 75e876825309c..8a1853812ed1e 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -18,3 +18,15 @@ CREATE FUNCTION test_dwb_ring_slots(current_only bool) CREATE FUNCTION test_dwb_states() RETURNS text STRICT AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_leak(npages int, do_publish bool) + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_force_seal() + RETURNS bool STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_retire() + RETURNS int STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index 53478228117ec..eaff938f6c0f4 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -157,6 +157,7 @@ test_dwb_ring_slots(PG_FUNCTION_ARGS) { char path[MAXPGPATH]; int fd; + ssize_t r; DWBBatchHeader hdr; snprintf(path, MAXPGPATH, DWB_DIR "/batch_%04u", b); @@ -166,20 +167,56 @@ test_dwb_ring_slots(PG_FUNCTION_ARGS) (errcode_for_file_access(), errmsg("could not open file \"%s\": %m", path))); - if (pg_pread(fd, &hdr, sizeof(hdr), 0) != sizeof(hdr)) - ereport(ERROR, (errmsg("short read of header in \"%s\"", path))); + errno = 0; + r = pg_pread(fd, &hdr, sizeof(hdr), 0); + if (r != sizeof(hdr)) + { + if (r < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", path))); + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("short read of header in \"%s\"", path))); + } /* an all-zero (never written) batch fails the header check */ if (hdr.magic == DWB_BATCH_MAGIC && - EQ_CRC32C(hdr.crc, DWBBatchHeaderCrc(&hdr)) && - hdr.n_slots <= control.batch_pages) + EQ_CRC32C(hdr.crc, DWBBatchHeaderCrc(&hdr))) { - int nbytes = hdr.n_slots * sizeof(DWSlotMeta); + ssize_t nbytes = hdr.n_slots * sizeof(DWSlotMeta); + + /* + * A CRC-valid header with out-of-range n_slots cannot happen + * under the startup geometry check; report the anomaly instead + * of silently contributing zero slots. + */ + if (hdr.n_slots > control.batch_pages) + { + ereport(WARNING, + (errmsg("batch file \"%s\" has out-of-range n_slots %u", + path, hdr.n_slots))); + nbytes = -1; + } - if (pg_pread(fd, metas, nbytes, sizeof(DWBBatchHeader)) != nbytes) - ereport(ERROR, (errmsg("short read of metas in \"%s\"", path))); + if (nbytes >= 0) + { + errno = 0; + r = pg_pread(fd, metas, nbytes, sizeof(DWBBatchHeader)); + if (r != nbytes) + { + if (r < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read metas in \"%s\": %m", + path))); + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("short read of metas in \"%s\"", path))); + } + } - for (uint32 i = 0; i < hdr.n_slots; i++) + for (uint32 i = 0; nbytes >= 0 && i < hdr.n_slots; i++) { DWSlotMeta *meta = &metas[i]; @@ -190,9 +227,20 @@ test_dwb_ring_slots(PG_FUNCTION_ARGS) if (current_only && meta->generation != control.generation) continue; - if (pg_pread(fd, image, BLCKSZ, - meta_region + (off_t) i * BLCKSZ) != BLCKSZ) - ereport(ERROR, (errmsg("short read of image in \"%s\"", path))); + errno = 0; + r = pg_pread(fd, image, BLCKSZ, + meta_region + (off_t) i * BLCKSZ); + if (r != BLCKSZ) + { + if (r < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read image in \"%s\": %m", + path))); + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("short read of image in \"%s\"", path))); + } if (!EQ_CRC32C(meta->image_crc, DWBImageCrc(image))) continue; valid++; @@ -235,3 +283,58 @@ test_dwb_states(PG_FUNCTION_ARGS) counts[DWB_RETIRING] + counts[DWB_OOM_RETIRING]); PG_RETURN_TEXT_P(cstring_to_text(buf.data)); } + +/* + * Acquire (and optionally publish) npages slots and return WITHOUT + * releasing them: the refs stay pending, so closing the session exercises + * DWBProcExit's poison (unpublished) or orphan (published) path. + */ +PG_FUNCTION_INFO_V1(test_dwb_leak); +Datum +test_dwb_leak(PG_FUNCTION_ARGS) +{ + int npages = PG_GETARG_INT32(0); + bool do_publish = PG_GETARG_BOOL(1); + static char page[BLCKSZ]; + + check_dwb_enabled(); + /* stay below the batch size so this backend never seals as leader */ + if (npages < 1 || npages >= dwb_batch_pages) + ereport(ERROR, (errmsg("npages out of range"))); + + for (int i = 0; i < npages; i++) + { + BufferTag tag; + DWBSlotRef ref; + RelFileLocator rlocator; + + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = 1; + rlocator.relNumber = 91000; + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, (BlockNumber) i); + + DWBAcquireSlot(&tag, &ref); + if (do_publish) + { + memset(page, 'L', BLCKSZ); + DWBPublishImage(&ref, page, (XLogRecPtr) 0x2000000 + i); + } + } + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(test_dwb_force_seal); +Datum +test_dwb_force_seal(PG_FUNCTION_ARGS) +{ + check_dwb_enabled(); + PG_RETURN_BOOL(DWBForceSealOpenBatch(DWB_WCLASS_EVICTION)); +} + +PG_FUNCTION_INFO_V1(test_dwb_retire); +Datum +test_dwb_retire(PG_FUNCTION_ARGS) +{ + check_dwb_enabled(); + PG_RETURN_INT32(DWBRetireAllSync()); +} From 35828d8416d5129b3078c9cffda3b68f187ce137 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 24 Jul 2026 09:16:11 +0300 Subject: [PATCH 03/52] Fix ABA hijack of a reopened open batch in DWBOpenNewBatch The "has the open batch already been replaced" guard compared only the ring index. Indexes are reused quickly on a small ring, so a slow opener - one that bounced off a sealed batch and then slept, e.g. in the pre-lock staging-buffer wait - could arrive while open_batch_idx named the SAME index again, now holding a NEW live incarnation. The index-only comparison passed, the opener repointed open_batch_idx to a fresh batch, and the live incarnation was orphaned in ALLOCATED together with its staging buffer: nothing ever seals an unreachable batch, writers waiting on it hang, and a writer blocked there while holding an unpublished reservation in the next batch eventually drives that batch's leader into the coverage-wait PANIC. The concurrent TAP stress hit this in the majority of runs. Replace the guard with the exact criterion, evaluated under DWBRingOpenLock: the open batch needs replacing if and only if its SEAL_BIT is set. SEAL_BIT is set at SEAL and stays set through FREE; only the reopen re-initialization - under this same lock - clears it. A live reopened incarnation at the same index therefore reads as "bit clear" and the stale opener backs off; if the new incarnation has itself been sealed meanwhile, replacing it is correct no matter which incarnation the caller bounced off. Comparing batch ids instead would require capturing a non-atomic uint64 outside the lock (a torn-read window on 32-bit platforms) and plumbing it through the API. Add test_dwb_open_stale(): a deterministic single-backend regression that replays the losing interleaving without any timing dependence - cycle once so open_batch_idx names a sealed-and-retired index, reopen the same index as a live incarnation by acquiring one slot, then call DWBOpenNewBatch with the stale index and assert the pointer is untouched. With the index-only guard the test fails every run; the organic stress race fires only probabilistically. DWBOpenNewBatch is exported non-static solely for this test. --- src/backend/storage/dwb/dwb.c | 33 +++++++--- src/include/storage/dwb.h | 2 + src/test/modules/test_dwb/t/001_dwb.pl | 10 +++ src/test/modules/test_dwb/test_dwb--1.0.sql | 4 ++ src/test/modules/test_dwb/test_dwb.c | 69 +++++++++++++++++++++ 5 files changed, 111 insertions(+), 7 deletions(-) diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index ac2a2652e7bcd..fbb7ff052b103 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -123,8 +123,10 @@ DWBStagingRelease(int idx) * fetch_add against a reused batch either sees SEAL_BIT (and retries) or * lands on a valid slot of the new incarnation — never on a slot that a * concurrent reset can wipe. + * + * Non-static only for test_dwb's stale-open regression test. */ -static void +void DWBOpenNewBatch(int wclass, uint32 old_idx) { for (;;) @@ -142,13 +144,30 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) LWLockAcquire(DWBRingOpenLock, LW_EXCLUSIVE); - /* someone else already replaced the open batch: done */ - if (pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]) != old_idx) + /* + * Someone else already replaced the open batch: done. Comparing + * the index alone is not enough: the ring reuses indexes, so by the + * time a slow opener gets here, old_idx may name a NEW live + * incarnation of the same slot (sealed, retired, freed and reopened + * behind our back), and replacing it would orphan that live batch + * together with its staging buffer. SEAL_BIT disambiguates the + * incarnations: it is set from SEAL through FREE and cleared only + * by the re-initialization below, under this same lock — so the + * open batch needs replacing if and only if its SEAL_BIT is set. + */ { - LWLockRelease(DWBRingOpenLock); - DWBStagingRelease(staging_idx); - ConditionVariableCancelSleep(); - return; + uint32 cur = pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]); + + if (cur != old_idx || + (cur != DWB_INVALID_BATCH && + !(pg_atomic_read_u32(&DWBCtl->batches[cur].next_slot_idx) & + DWB_SEAL_BIT))) + { + LWLockRelease(DWBRingOpenLock); + DWBStagingRelease(staging_idx); + ConditionVariableCancelSleep(); + return; + } } for (int i = 0; i < dwb_num_batches; i++) diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 85e73b8170651..440082907e027 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -273,6 +273,8 @@ extern void DWBReleaseSlot(const DWBSlotRef *ref); extern bool DWBForceSealOpenBatch(int wclass); extern int DWBRetireAllSync(void); extern DWBatchState DWBGetBatchState(int batch_idx); +/* internal; exported for test_dwb's stale-open regression test */ +extern void DWBOpenNewBatch(int wclass, uint32 old_idx); /* dwb_file.c */ extern void DWBCreateRing(void); diff --git a/src/test/modules/test_dwb/t/001_dwb.pl b/src/test/modules/test_dwb/t/001_dwb.pl index 83766e7e1eb02..cd4540085998d 100644 --- a/src/test/modules/test_dwb/t/001_dwb.pl +++ b/src/test/modules/test_dwb/t/001_dwb.pl @@ -130,6 +130,16 @@ sub flip_byte $node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=16/, 'ring fully idle after the orphan hand-off'); +# --- stale open must not hijack a reopened index ------------------------ + +my ($rc, $out, $err) = + $node->psql('postgres', 'SELECT test_dwb_open_stale()'); +is($rc, 0, 'stale open leaves the live reopened batch alone') + or diag($err); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/free=16/, 'ring idle after the stale-open scenario'); + # --- geometry is fixed by the on-disk control file --------------------- $node->stop; diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index 8a1853812ed1e..12c5e3bc05507 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -30,3 +30,7 @@ CREATE FUNCTION test_dwb_force_seal() CREATE FUNCTION test_dwb_retire() RETURNS int STRICT AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_open_stale() + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index eaff938f6c0f4..b62cff325575a 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -338,3 +338,72 @@ test_dwb_retire(PG_FUNCTION_ARGS) check_dwb_enabled(); PG_RETURN_INT32(DWBRetireAllSync()); } + +/* + * Deterministic regression test for the stale-open ABA race: a writer that + * bounced off a sealed batch calls DWBOpenNewBatch only after the ring has + * reused the same index for a NEW live incarnation. The replacement guard + * must recognize the reuse and leave the live batch alone; the buggy + * index-only comparison would repoint open_batch_idx and orphan it. + * + * Single-backend and timing-free: we replay the loser's exact interleaving + * instead of racing two sessions. + */ +PG_FUNCTION_INFO_V1(test_dwb_open_stale); +Datum +test_dwb_open_stale(PG_FUNCTION_ARGS) +{ + uint32 stale_idx; + uint32 reopened_idx; + DWBSlotRef ref; + BufferTag tag; + RelFileLocator rlocator; + static char page[BLCKSZ]; + + check_dwb_enabled(); + + /* + * Cycle once so the open batch goes through seal and retire: + * open_batch_idx afterwards still names it, sealed (SEAL_BIT is held + * through FREE) — exactly a bounced writer's stale view. + */ + (void) dwb_cycle_internal(1); + stale_idx = pg_atomic_read_u32(&DWBCtl->open_batch_idx[DWB_WCLASS_EVICTION]); + + /* + * Acquire one slot: the fetch_add bounces on SEAL_BIT and reopens the + * lowest FREE index — the same index again, as a new live incarnation. + */ + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = 1; + rlocator.relNumber = 92000; + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); + DWBAcquireSlot(&tag, &ref); + memset(page, 'S', BLCKSZ); + DWBPublishImage(&ref, page, (XLogRecPtr) 0x3000000); + + reopened_idx = (uint32) ref.batch_idx; + if (reopened_idx != stale_idx) + ereport(ERROR, + (errmsg("stale-open scenario not reproduced: reopened %u, stale %u", + reopened_idx, stale_idx))); + + /* + * The ABA moment: a stale opener calls with old_idx naming the live + * reopened incarnation. The guard must not replace it. + */ + DWBOpenNewBatch(DWB_WCLASS_EVICTION, stale_idx); + + if (pg_atomic_read_u32(&DWBCtl->open_batch_idx[DWB_WCLASS_EVICTION]) != + reopened_idx) + ereport(ERROR, + (errmsg("stale open hijacked the live open batch"))); + + /* drain: seal, wait durable, release, retire */ + (void) DWBForceSealOpenBatch(DWB_WCLASS_EVICTION); + DWBWaitBatchFsynced(&ref); + DWBReleaseSlot(&ref); + (void) DWBRetireAllSync(); + + PG_RETURN_VOID(); +} From 1d3843acf757f02b111557dbab05d3d28e31f0e6 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 24 Jul 2026 09:24:02 +0300 Subject: [PATCH 04/52] Remove unused DWBCloseBatchFiles No caller in any planned stage: the apply-pass and ring re-creation run in the startup process before any backend exists, and the batch-file VFD caches of leaders and retire workers are torn down by fd.c at process exit anyway. --- src/backend/storage/dwb/dwb_file.c | 15 --------------- src/include/storage/dwb.h | 1 - 2 files changed, 16 deletions(-) diff --git a/src/backend/storage/dwb/dwb_file.c b/src/backend/storage/dwb/dwb_file.c index aa1d5c34d4a28..74359d08e24c2 100644 --- a/src/backend/storage/dwb/dwb_file.c +++ b/src/backend/storage/dwb/dwb_file.c @@ -275,21 +275,6 @@ DWBOpenBatchFile(int batch_idx) return batch_files[batch_idx]; } -void -DWBCloseBatchFiles(void) -{ - if (batch_files == NULL) - return; - for (int i = 0; i < dwb_num_batches; i++) - { - if (batch_files[i] >= 0) - { - FileClose(batch_files[i]); - batch_files[i] = -1; - } - } -} - /* * Pre-open the batch file and pre-allocate the meta-region buffer, so that * DWBWriteBatch can run inside the leader's critical section without diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 440082907e027..6beb2679c3d06 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -281,7 +281,6 @@ extern void DWBCreateRing(void); extern bool DWBReadControlFile(DWBControlFileData *control, bool missing_ok); extern void DWBWriteControlFile(const DWBControlFileData *control); extern int DWBOpenBatchFile(int batch_idx); -extern void DWBCloseBatchFiles(void); extern void DWBPrepareBatchWrite(int batch_idx); extern void DWBWriteBatch(int batch_idx, const DWBBatchHeader *hdr, const DWSlotMeta *metas, const char *images); From 21da0b0975956f7a7ef796c4cf59dd5c02dec496 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 24 Jul 2026 15:26:32 +0300 Subject: [PATCH 05/52] Integrate the DWB into FlushBuffer and add real retirement (Stage 2) FlushBuffer now stages every BM_PERMANENT page into the double write buffer before smgrwrite: DWBStagePageWrite makes the private copy durable in pg_dwb/, DWBFinishPageWrite releases the batch ref afterwards, with an optional smgrwriteback in between (dwb_writeback) so the retirement fsync becomes a cheap barrier. Writers are classified by process role into the eviction and background (checkpointer, bgwriter) classes with separate open batches; background opens keep DWB_EVICT_RESERVE free batches for user evictions. Retirement is driven by the new segment->batch back-reference hash (DWSegmentHash): the last batch ref publishes the seg_set under the batch's publish_lock, and any segment fsync clears the bits it had snapshotted before starting, guarded against ring-index reuse by re-checking batch_id under the same lock. Fsyncs come from three independent sources sharing that accounting: a pool of dwb_retire_workers background workers (partitioned proactive sweeps, force-seal of timed-out batches), ProcessSyncRequests in the checkpointer (wrapped around each md segment fsync), and writers stuck on a full ring sweeping synchronously as self-service. A hash overflow degrades to a synchronous per-batch retire instead of wedging the ring. Sealing is decentralized: a writer whose ref is the only one on a still-open batch seals immediately (a lone sequential flush stream would otherwise pay dwb_batch_timeout_ms per page), a waiter seals after that timeout, and the workers back-stop batches whose writers all died. This keeps the write path fully functional with no worker pool at all (dwb_retire_workers = 0, single-user mode, shutdown checkpoint after the workers are gone). Refs are now attached to the ResourceOwner and released before the buffer-IO cleanup: an abandoned ref whose batch is already durable repairs its data page from the batch file while BM_IO_IN_PROGRESS is still held, closing the window where a half-done smgrwrite could leave a torn page behind a recycling batch, without racing any concurrent flush. Backpressure escalates in two stages clocked by retirement progress (freed_events): a WARNING plus bgwriter pause, then dwb_on_stall, where the checkpointer and the startup process always PANIC by explicit policy. The dwb-force-stall injection point drives both TAP scenarios: a stalled backend eviction fails with an ERROR while the cluster stays up, and a stalled checkpointer PANICs into crash recovery. pg_stat_io gains the "dwb" object (batch writes with bytes, fdatasyncs); new wait events cover the worker main loop and batch-file reads. --- src/backend/postmaster/bgworker.c | 4 + src/backend/postmaster/bgwriter.c | 11 +- src/backend/postmaster/postmaster.c | 7 + src/backend/storage/buffer/bufmgr.c | 30 + src/backend/storage/dwb/Makefile | 3 +- src/backend/storage/dwb/dwb.c | 626 +++++++++++++--- src/backend/storage/dwb/dwb_ctl.c | 29 +- src/backend/storage/dwb/dwb_file.c | 41 ++ src/backend/storage/dwb/dwb_retire.c | 675 ++++++++++++++++++ src/backend/storage/dwb/meson.build | 1 + src/backend/storage/sync/sync.c | 14 + src/backend/utils/activity/pgstat_io.c | 15 + .../utils/activity/wait_event_names.txt | 3 + src/backend/utils/misc/guc_tables.c | 6 +- src/include/pgstat.h | 3 +- src/include/storage/dwb.h | 85 ++- src/include/storage/lwlocklist.h | 1 + src/test/modules/test_dwb/Makefile | 4 + src/test/modules/test_dwb/t/001_dwb.pl | 46 +- .../modules/test_dwb/t/002_flushbuffer.pl | 82 +++ .../modules/test_dwb/t/003_backpressure.pl | 126 ++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 12 + src/test/modules/test_dwb/test_dwb.c | 143 +++- src/test/modules/test_dwb/test_dwb.conf | 4 + src/test/regress/expected/stats.out | 13 +- 25 files changed, 1832 insertions(+), 152 deletions(-) create mode 100644 src/backend/storage/dwb/dwb_retire.c create mode 100644 src/test/modules/test_dwb/t/002_flushbuffer.pl create mode 100644 src/test/modules/test_dwb/t/003_backpressure.pl diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c index 1ad65c237c34e..8524dd0573296 100644 --- a/src/backend/postmaster/bgworker.c +++ b/src/backend/postmaster/bgworker.c @@ -21,6 +21,7 @@ #include "postmaster/postmaster.h" #include "replication/logicallauncher.h" #include "replication/logicalworker.h" +#include "storage/dwb.h" #include "storage/ipc.h" #include "storage/latch.h" #include "storage/lwlock.h" @@ -124,6 +125,9 @@ static const struct { "ApplyLauncherMain", ApplyLauncherMain }, + { + "DWBRetireWorkerMain", DWBRetireWorkerMain + }, { "ApplyWorkerMain", ApplyWorkerMain }, diff --git a/src/backend/postmaster/bgwriter.c b/src/backend/postmaster/bgwriter.c index 72f5acceec78d..81e9cdaf2b6c2 100644 --- a/src/backend/postmaster/bgwriter.c +++ b/src/backend/postmaster/bgwriter.c @@ -42,6 +42,7 @@ #include "storage/buf_internals.h" #include "storage/bufmgr.h" #include "storage/condition_variable.h" +#include "storage/dwb.h" #include "storage/fd.h" #include "storage/lwlock.h" #include "storage/proc.h" @@ -231,9 +232,15 @@ BackgroundWriterMain(const void *startup_data, size_t startup_data_len) ProcessMainLoopInterrupts(); /* - * Do one cycle of dirty-buffer writing. + * Do one cycle of dirty-buffer writing. While a double write + * buffer stall has us paused (Stage A backpressure), sit the round + * out instead of queueing more flushes behind an exhausted ring; + * user-facing paths keep their reserve, we retry after the delay. */ - can_hibernate = BgBufferSync(&wb_context); + if (DWBIsEnabled() && DWBWritesPaused()) + can_hibernate = false; + else + can_hibernate = BgBufferSync(&wb_context); /* Report pending statistics to the cumulative stats system */ pgstat_report_bgwriter(); diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index 5124d39e5c2fb..170cdc67dfc27 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -110,6 +110,7 @@ #include "replication/slotsync.h" #include "replication/walsender.h" #include "storage/aio_subsys.h" +#include "storage/dwb.h" #include "storage/fd.h" #include "storage/io_worker.h" #include "storage/ipc.h" @@ -927,6 +928,12 @@ PostmasterMain(int argc, char *argv[]) */ ApplyLauncherRegister(); + /* + * Register the double write buffer retire workers, for the same + * reason: the ring cannot circulate without them. + */ + DWBRetireWorkersRegister(); + /* * process any libraries that should be preloaded at postmaster start */ diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 27fd7e9720a48..4b349ca9ee275 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/dwb.h" #include "storage/fd.h" #include "storage/ipc.h" #include "storage/lmgr.h" @@ -4299,6 +4300,7 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, Block bufBlock; char *bufToWrite; uint32 buf_state; + DWBSlotRef dwbref; /* * Try to start an I/O operation. If StartBufferIO returns false, then @@ -4371,6 +4373,21 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, */ bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum); + /* + * Double write buffer path: before the data-file write, make the copy + * durable in pg_dwb/ so that a torn smgrwrite can always be repaired + * from there (full_page_writes replacement, see storage/dwb.h). Only + * BM_PERMANENT buffers need this: unlogged relations are reset from + * their init fork after a crash, so their torn writes don't matter. + * With data checksums required by the DWB, bufToWrite is always a + * private copy, stable regardless of concurrent hint-bit updates. + */ + if (DWBIsEnabled() && (buf_state & BM_PERMANENT) && + !IsBootstrapProcessingMode()) + DWBStagePageWrite(&buf->tag, bufToWrite, recptr, &dwbref); + else + dwbref.batch_idx = -1; + io_start = pgstat_prepare_io_time(track_io_timing); /* @@ -4403,6 +4420,19 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, pgstat_count_io_op_time(IOOBJECT_RELATION, io_context, IOOP_WRITE, io_start, 1, BLCKSZ); + if (dwbref.batch_idx >= 0) + { + /* + * Step 6b: start kernel writeback of the page now so the segment + * fsync that retires the batch becomes a cheap barrier instead of + * a full flush. Not durability — that comes from the fsync. + */ + if (dwb_writeback) + smgrwriteback(reln, BufTagGetForkNum(&buf->tag), + buf->tag.blockNum, 1); + DWBFinishPageWrite(&dwbref); + } + pgBufferUsage.shared_blks_written++; /* diff --git a/src/backend/storage/dwb/Makefile b/src/backend/storage/dwb/Makefile index c45c8b16442a8..fbbb1a3f93c8f 100644 --- a/src/backend/storage/dwb/Makefile +++ b/src/backend/storage/dwb/Makefile @@ -16,6 +16,7 @@ OBJS = \ dwb.o \ dwb_ctl.o \ dwb_file.o \ - dwb_recovery.o + dwb_recovery.o \ + dwb_retire.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index fbb7ff052b103..e4b0bd581be60 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -13,9 +13,8 @@ * meta region, then fdatasync (the on-disk layout puts the meta region * first; see dwb.h) — and broadcasts DWB_FSYNCED. * - * Stage 1 scope: the state machine is complete but not yet wired into - * FlushBuffer; retirement is synchronous (DWBRetireAllSync) — the retire - * worker pool and the segment back-reference hash arrive in Stage 2. + * FlushBuffer drives this through DWBStagePageWrite/DWBFinishPageWrite; + * retirement (segment fsyncs, the worker pool) lives in dwb_retire.c. * * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California @@ -27,33 +26,82 @@ */ #include "postgres.h" +#include +#include +#include + +#include "common/relpath.h" #include "miscadmin.h" #include "port/pg_bitutils.h" #include "storage/dwb.h" +#include "storage/fd.h" #include "storage/ipc.h" #include "storage/proc.h" +#include "utils/injection_point.h" #include "utils/memutils.h" +#include "utils/resowner.h" #include "utils/timestamp.h" #include "utils/wait_event.h" /* - * Slot refs held by this backend, for cleanup on process exit. A ref lives - * from DWBAcquireSlot to DWBReleaseSlot. Sized to two full batches because - * a backend can hold refs on a sealed batch and on its successor at the - * same time, releasing the former only after the switch. (Stage 2 - * additionally attaches refs to the ResourceOwner so that a transaction - * abort — e.g. an ERROR out of smgrwrite — releases them too.) + * Slot refs held by this backend. A ref lives from DWBAcquireSlot to + * DWBReleaseSlot; the write path also attaches it to the current + * ResourceOwner, so that a transaction abort (e.g. an ERROR out of + * smgrwrite) releases it long before process exit. Entries have stable + * addresses — the ResourceOwner remembers a pointer — so freeing is a flag, + * not compaction. Sized to two full batches because a backend can hold + * refs on a sealed batch and on its successor at the same time. */ -static DWBSlotRef pendingRefs[2 * DWB_BATCH_MAX_PAGES]; +typedef struct DWBPendingRef +{ + DWBSlotRef ref; + ResourceOwner owner; /* owner the ref is registered with, or NULL */ + bool in_use; +} DWBPendingRef; + +static DWBPendingRef pendingRefs[2 * DWB_BATCH_MAX_PAGES]; static int nPendingRefs = 0; static bool cleanup_registered = false; /* leader-side meta assembly area, allocated before the seal is attempted */ static DWSlotMeta *leader_metas = NULL; +/* Stage A pause of the bgwriter (see DWBWritesPaused) */ +static bool bgwriter_paused = false; +static uint64 bgwriter_pause_snap = 0; + +/* + * Escalation clock of one wait for ring space. The clock re-arms whenever + * freed_events moves: escalation fires only when retirement as a whole has + * made no progress for the full window, i.e. "broken", not "slow". + */ +typedef struct DWBStallState +{ + TimestampTz start; + uint64 freed_snap; + bool warned; +} DWBStallState; + static void DWBProcExit(int code, Datum arg); static void DWBLeaderWriteBatch(int batch_idx); +static bool DWBSealBatch(int batch_idx); static void DWBFinishBatchData(DWBatchCtl *batch); +static void DWBAbandonRef(DWBPendingRef *pref); +static void ResOwnerReleaseDWBRef(Datum res); + +/* + * Released BEFORE the buffer-IO cleanup (smaller priority runs first): + * DWBAbandonRef may repair the data page from the batch file, which is only + * race-free while BM_IO_IN_PROGRESS of the aborted flush is still ours. + */ +static const ResourceOwnerDesc dwb_ref_resowner_desc = +{ + .name = "double write buffer slot ref", + .release_phase = RESOURCE_RELEASE_BEFORE_LOCKS, + .release_priority = RELEASE_PRIO_BUFFER_IOS - 10, + .ReleaseResource = ResOwnerReleaseDWBRef, + .DebugPrint = NULL, +}; static inline char * DWBStagingSlotPtr(int staging_idx, int slot_idx) @@ -63,6 +111,102 @@ DWBStagingSlotPtr(int staging_idx, int slot_idx) (Size) slot_idx * BLCKSZ; } +/* ---------------------------------------------------------------- + * backpressure (3.6) + * ---------------------------------------------------------------- + */ + +static void +DWBStallInit(DWBStallState *st) +{ + st->start = GetCurrentTimestamp(); + st->freed_snap = pg_atomic_read_u64(&DWBCtl->freed_events); + st->warned = false; +} + +/* + * Escalate one iteration of a ring-space wait. Stage A after + * dwb_slow_warn_ms: WARNING, and the bgwriter additionally pauses its own + * future flush rounds (DWBWritesPaused). Stage B after + * dwb_write_timeout_ms without a single retired batch: dwb_on_stall, except + * that the checkpointer and the startup process always PANIC — an ERROR + * there would fail the checkpoint or recovery anyway, without the fresh + * start that crash recovery gives (the explicit safety policy of 3.6, + * consistent with data_sync_elevel for a checkpoint-phase fsync failure). + * + * The dwb-force-stall injection point makes the current wait escalate to + * Stage B immediately: the role policy and dwb_on_stall handling stay + * exactly the production code paths, only the clock is bypassed. + */ +static void +DWBStallCheck(DWBStallState *st) +{ + uint64 freed = pg_atomic_read_u64(&DWBCtl->freed_events); + TimestampTz now = GetCurrentTimestamp(); + long waited; + bool forced; + + if (freed != st->freed_snap) + { + /* retirement made progress: re-arm */ + st->freed_snap = freed; + st->start = now; + st->warned = false; + return; + } + + waited = TimestampDifferenceMilliseconds(st->start, now); + forced = IS_INJECTION_POINT_ATTACHED("dwb-force-stall"); + + if (waited >= dwb_slow_warn_ms && !st->warned) + { + st->warned = true; + ereport(WARNING, + (errmsg("double write buffer has no free batch after %ld ms", + waited))); + if (MyBackendType == B_BG_WRITER) + { + bgwriter_paused = true; + bgwriter_pause_snap = freed; + } + } + + if (waited >= dwb_write_timeout_ms || forced) + { + if (AmCheckpointerProcess() || AmStartupProcess() || + CritSectionCount > 0 || dwb_on_stall == DWB_ON_STALL_PANIC) + ereport(PANIC, + (errmsg("double write buffer retirement made no progress within \"dwb_write_timeout_ms\""), + errdetail("No batch was retired while a %s process waited for ring space.", + GetBackendTypeDesc(MyBackendType)))); + if (dwb_on_stall == DWB_ON_STALL_ERROR) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("double write buffer retirement made no progress within \"dwb_write_timeout_ms\""))); + /* DWB_ON_STALL_WARN: complain and keep waiting */ + ereport(WARNING, + (errmsg("double write buffer retirement made no progress within \"dwb_write_timeout_ms\", still waiting"))); + st->start = now; + } +} + +/* + * Has a Stage A stall told the bgwriter to sit out its flush rounds? + * Clears itself as soon as any batch retires. + */ +bool +DWBWritesPaused(void) +{ + if (!bgwriter_paused) + return false; + if (pg_atomic_read_u64(&DWBCtl->freed_events) != bgwriter_pause_snap) + { + bgwriter_paused = false; + return false; + } + return true; +} + /* ---------------------------------------------------------------- * staging pool * ---------------------------------------------------------------- @@ -71,7 +215,9 @@ static int DWBStagingAlloc(void) { int idx; + DWBStallState stall; + DWBStallInit(&stall); for (;;) { idx = -1; @@ -90,10 +236,11 @@ DWBStagingAlloc(void) /* * A buffer frees once its leader finishes the image pwrite; * retirement broadcasts cv_free_batch too, so just re-check on - * every wake-up. + * every wake-up. The timeout only paces the stall clock. */ - ConditionVariableSleep(&DWBCtl->cv_free_batch, - WAIT_EVENT_DWB_FREE_BATCH); + (void) ConditionVariableTimedSleep(&DWBCtl->cv_free_batch, 1000, + WAIT_EVENT_DWB_FREE_BATCH); + DWBStallCheck(&stall); } ConditionVariableCancelSleep(); return idx; @@ -129,9 +276,13 @@ DWBStagingRelease(int idx) void DWBOpenNewBatch(int wclass, uint32 old_idx) { + DWBStallState stall; + + DWBStallInit(&stall); for (;;) { int free_idx = -1; + int nfree = 0; int staging_idx; /* @@ -170,16 +321,32 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) } } + /* + * Count FREE batches first: a background-class open must leave + * DWB_EVICT_RESERVE of them for user evictions, so that a + * checkpoint's BufferSync storm cannot eat the ring from under + * latency-critical paths. FREE->ALLOCATED happens only under + * DWBRingOpenLock, and concurrent retirements only grow the count, + * so the check cannot overestimate. + */ for (int i = 0; i < dwb_num_batches; i++) - { - uint32 expected = DWB_FREE; + if (pg_atomic_read_u32(&DWBCtl->batches[i].state) == DWB_FREE) + nfree++; - if (pg_atomic_compare_exchange_u32(&DWBCtl->batches[i].state, - &expected, DWB_ALLOCATED)) + if (nfree > (wclass == DWB_WCLASS_BACKGROUND ? DWB_EVICT_RESERVE : 0)) + { + for (int i = 0; i < dwb_num_batches; i++) { - free_idx = i; - break; + uint32 expected = DWB_FREE; + + if (pg_atomic_compare_exchange_u32(&DWBCtl->batches[i].state, + &expected, DWB_ALLOCATED)) + { + free_idx = i; + break; + } } + Assert(free_idx >= 0); } if (free_idx >= 0) @@ -191,7 +358,6 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) pg_atomic_write_u32(&batch->capped_slots, 0); pg_atomic_write_u32(&batch->ref_count, 0); pg_atomic_write_u32(&batch->seg_pending_count, 0); - pg_atomic_write_u32(&batch->orphaned_refs_count, 0); batch->n_segs = 0; batch->max_page_lsn = InvalidXLogRecPtr; batch->batch_id = pg_atomic_fetch_add_u64(&DWBCtl->next_batch_id, 1); @@ -208,15 +374,29 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) pg_atomic_write_u32(&DWBCtl->open_batch_idx[wclass], free_idx); LWLockRelease(DWBRingOpenLock); ConditionVariableCancelSleep(); + + /* let retire workers re-time the force-seal deadline */ + ConditionVariableBroadcast(&DWBCtl->cv_retire_wake); return; } LWLockRelease(DWBRingOpenLock); DWBStagingRelease(staging_idx); - /* whole ring busy: wait for a retirement, then retry */ - ConditionVariableSleep(&DWBCtl->cv_free_batch, - WAIT_EVENT_DWB_FREE_BATCH); + /* + * No usable FREE batch. Help ourselves before waiting: sweep the + * RETIRING batches synchronously. Under normal operation the + * worker pool keeps the ring ahead of the writers and this path is + * rare; when it does run, the per-segment claim keeps us and the + * workers from duplicating fsyncs. This is also what keeps the + * ring alive with dwb_retire_workers = 0 and in single-user mode. + */ + if (DWBRetireAllSync() > 0) + continue; + + (void) ConditionVariableTimedSleep(&DWBCtl->cv_free_batch, 1000, + WAIT_EVENT_DWB_FREE_BATCH); + DWBStallCheck(&stall); } } @@ -288,6 +468,7 @@ DWBSealBatch(int batch_idx) DWBStagingRelease(batch->staging_idx); batch->staging_idx = -1; pg_atomic_write_u32(&batch->state, DWB_FREE); + pg_atomic_fetch_add_u64(&DWBCtl->freed_events, 1); ConditionVariableBroadcast(&DWBCtl->cv_free_batch); END_CRIT_SECTION(); return true; @@ -419,20 +600,61 @@ DWBLeaderWriteBatch(int batch_idx) */ /* - * Reserve a slot in the open batch (Stage 1: single writer class), record - * the page tag and the segment ref, and take a batch ref. + * Try to seal a batch if it is still an open, non-empty ALLOCATED one. + * Every seal initiator goes through here: overflow writers, retire workers + * acting on dwb_batch_timeout_ms, and writers whose DWBWaitBatchFsynced + * timed out on a batch nobody else sealed. + */ +bool +DWBTrySealBatch(int batch_idx) +{ + DWBatchCtl *batch = &DWBCtl->batches[batch_idx]; + uint32 nsi = pg_atomic_read_u32(&batch->next_slot_idx); + + if (nsi & DWB_SEAL_BIT) + return false; /* sealed already (or FREE: the bit is held + * through FREE until reopen) */ + if ((nsi & DWB_IDX_MASK) == 0) + return false; /* empty: sealing buys nothing */ + if (pg_atomic_read_u32(&batch->state) != DWB_ALLOCATED) + return false; + return DWBSealBatch(batch_idx); +} + +/* + * Reserve a slot in the open batch of the given writer class, record the + * page tag and the segment ref, and take a batch ref. With use_resowner + * the ref is also attached to CurrentResourceOwner, so a transaction abort + * releases it (the write path always does this; tests exercising proc-exit + * cleanup do not). */ void -DWBAcquireSlot(const BufferTag *tag, DWBSlotRef *ref) +DWBAcquireSlot(const BufferTag *tag, int wclass, bool use_resowner, + DWBSlotRef *ref) { - const int wclass = DWB_WCLASS_EVICTION; + DWBPendingRef *pref = NULL; Assert(DWBIsEnabled()); + Assert(wclass >= 0 && wclass < DWB_NUM_WCLASSES); /* hard bound: overflowing the static array would corrupt memory */ if (nPendingRefs >= (int) lengthof(pendingRefs)) elog(ERROR, "too many pending double write buffer slot refs held by one backend"); + for (int i = 0; i < (int) lengthof(pendingRefs); i++) + { + if (!pendingRefs[i].in_use) + { + pref = &pendingRefs[i]; + break; + } + } + Assert(pref != NULL); + + /* no failure window between the reservation below and remembering it */ + if (use_resowner) + ResourceOwnerEnlarge(CurrentResourceOwner); + if (!cleanup_registered) { on_proc_exit(DWBProcExit, 0); @@ -508,7 +730,14 @@ DWBAcquireSlot(const BufferTag *tag, DWBSlotRef *ref) ref->batch_idx = (int) idx; ref->slot_idx = (int) slot; ref->batch_id = batch->batch_id; - pendingRefs[nPendingRefs++] = *ref; + + pref->ref = *ref; + pref->owner = use_resowner ? CurrentResourceOwner : NULL; + pref->in_use = true; + nPendingRefs++; + if (pref->owner) + ResourceOwnerRemember(pref->owner, PointerGetDatum(pref), + &dwb_ref_resowner_desc); return; } } @@ -539,6 +768,12 @@ DWBPublishImage(const DWBSlotRef *ref, const char *image, XLogRecPtr page_lsn) /* * Wait until the batch's DWB copy is durable. The caller holds a batch * ref, so the batch cannot be retired or reused under us. + * + * A batch that nobody seals would leave its writers waiting forever, so + * after dwb_batch_timeout_ms of waiting on a still-open batch the waiter + * seals it itself. The retire workers force-seal on the same timeout; + * this decentralized backstop keeps the write path independent of the + * worker pool (dwb_retire_workers = 0, single-user mode, a stuck worker). */ void DWBWaitBatchFsynced(const DWBSlotRef *ref) @@ -547,17 +782,34 @@ DWBWaitBatchFsynced(const DWBSlotRef *ref) Assert(ref->batch_id == batch->batch_id); + /* + * A lone writer has nobody to batch with: sequential flush streams + * (recovery, a backend evicting page after page, BufferSync) reach + * this wait one page at a time, and paying dwb_batch_timeout_ms per + * page would dominate the stream. If our ref is the only one on a + * still-open batch, seal right away; under concurrency ref_count > 1 + * keeps the rendezvous window open for the timeout. A racing second + * writer merely bounces to the next batch — sealing is valid at any + * moment. + */ + if (pg_atomic_read_u32(&batch->ref_count) == 1) + (void) DWBTrySealBatch(ref->batch_idx); + ConditionVariablePrepareToSleep(&batch->cv_state); while (pg_atomic_read_u32(&batch->state) < DWB_FSYNCED) - ConditionVariableSleep(&batch->cv_state, WAIT_EVENT_DWB_BATCH_FSYNC); + { + if (ConditionVariableTimedSleep(&batch->cv_state, + dwb_batch_timeout_ms, + WAIT_EVENT_DWB_BATCH_FSYNC)) + (void) DWBTrySealBatch(ref->batch_idx); + } ConditionVariableCancelSleep(); } /* * Step 7 of the write path: the last ref hands the batch over to - * retirement (FSYNCED -> DATA_WRITTEN -> RETIRING). (Stage 2 publishes - * the segment set into DWSegmentHash here; Stage 1 retirement is - * DWBRetireAllSync.) + * retirement — FSYNCED -> DATA_WRITTEN, then the seg_set publication into + * DWSegmentHash and the RETIRING transition (dwb_retire.c). */ static void DWBFinishBatchData(DWBatchCtl *batch) @@ -568,14 +820,7 @@ DWBFinishBatchData(DWBatchCtl *batch) DWB_DATA_WRITTEN)) elog(PANIC, "DWB batch data-written in unexpected state %u", expected); - pg_atomic_write_u32(&batch->seg_pending_count, batch->n_segs); - pg_write_barrier(); - - expected = DWB_DATA_WRITTEN; - if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, - DWB_RETIRING)) - elog(PANIC, "DWB batch retiring in unexpected state %u", expected); - ConditionVariableSignal(&DWBCtl->cv_retire_wake); + DWBPublishBatchSegSet((int) (batch - DWBCtl->batches)); } /* @@ -589,12 +834,20 @@ DWBReleaseSlot(const DWBSlotRef *ref) Assert(ref->batch_id == batch->batch_id); - for (int i = 0; i < nPendingRefs; i++) + for (int i = 0; i < (int) lengthof(pendingRefs); i++) { - if (pendingRefs[i].batch_idx == ref->batch_idx && - pendingRefs[i].slot_idx == ref->slot_idx) + DWBPendingRef *pref = &pendingRefs[i]; + + if (pref->in_use && + pref->ref.batch_idx == ref->batch_idx && + pref->ref.slot_idx == ref->slot_idx) { - pendingRefs[i] = pendingRefs[--nPendingRefs]; + if (pref->owner != NULL) + ResourceOwnerForget(pref->owner, PointerGetDatum(pref), + &dwb_ref_resowner_desc); + pref->owner = NULL; + pref->in_use = false; + nPendingRefs--; break; } } @@ -604,9 +857,8 @@ DWBReleaseSlot(const DWBSlotRef *ref) } /* - * Force-seal the currently open batch of a writer class (used by tests - * now; the retire worker's dwb_batch_timeout_ms path in Stage 2). - * Returns true if a batch was sealed by us. + * Force-seal the currently open batch of a writer class. Returns true if + * a batch was sealed by us. */ bool DWBForceSealOpenBatch(int wclass) @@ -615,99 +867,247 @@ DWBForceSealOpenBatch(int wclass) if (idx == DWB_INVALID_BATCH) return false; - if (pg_atomic_read_u32(&DWBCtl->batches[idx].next_slot_idx) == 0) - return false; /* empty batch: nothing to seal */ - return DWBSealBatch((int) idx); + return DWBTrySealBatch((int) idx); +} + +DWBatchState +DWBGetBatchState(int batch_idx) +{ + return (DWBatchState) pg_atomic_read_u32(&DWBCtl->batches[batch_idx].state); } +/* ---------------------------------------------------------------- + * FlushBuffer entry points + * ---------------------------------------------------------------- + */ + /* - * Synchronously retire every RETIRING batch. Stage 1: batch images are - * already durable in the ring and the test pages have no real relation - * segments to fsync, so retirement is pure state bookkeeping. Stage 2 - * replaces this with segment fsyncs by the retire worker pool and - * ProcessSyncRequests. + * The checkpointer's BufferSync and the bgwriter's flush rounds form the + * background stream; everything else — ordinary backend evictions above + * all — is the latency-critical class with first claim on FREE batches. */ -int -DWBRetireAllSync(void) +static int +DWBWriterClass(void) { - int retired = 0; + if (MyBackendType == B_CHECKPOINTER || MyBackendType == B_BG_WRITER) + return DWB_WCLASS_BACKGROUND; + return DWB_WCLASS_EVICTION; +} - for (int i = 0; i < dwb_num_batches; i++) - { - DWBatchCtl *batch = &DWBCtl->batches[i]; - uint32 expected = DWB_RETIRING; +/* + * Steps 3-5 of the write path (3.4): reserve a slot in this writer class's + * open batch, publish the private page copy, and wait until the batch copy + * is durable in pg_dwb/. On return the caller may write the same copy to + * the data file. The caller must already have flushed WAL up to page_lsn. + */ +void +DWBStagePageWrite(const BufferTag *tag, const char *image, + XLogRecPtr page_lsn, DWBSlotRef *ref) +{ + Assert(DWBCtl->ring_generation > 0); - if (pg_atomic_read_u32(&batch->state) != DWB_RETIRING) - continue; + DWBAcquireSlot(tag, DWBWriterClass(), true, ref); + DWBPublishImage(ref, image, page_lsn); - /* Stage 2: smgrimmedsync of each seg_set entry goes here */ + /* + * With no worker pool (dwb_retire_workers = 0, single-user mode) a + * lonely batch would only seal via the wait timeout below; seal it + * right away instead of paying dwb_batch_timeout_ms per page. + */ + if (dwb_retire_workers == 0 || !IsUnderPostmaster) + (void) DWBTrySealBatch(ref->batch_idx); - pg_atomic_write_u32(&batch->seg_pending_count, 0); - if (pg_atomic_compare_exchange_u32(&batch->state, &expected, - DWB_FREE)) - { - retired++; - ConditionVariableBroadcast(&DWBCtl->cv_free_batch); - } - } - return retired; + DWBWaitBatchFsynced(ref); + + INJECTION_POINT("dwb-after-batch-fsynced", NULL); } -DWBatchState -DWBGetBatchState(int batch_idx) +/* + * Step 7: release the ref after smgrwrite returned. Without a worker + * pool, also retire synchronously so the ring keeps circulating (and, in + * the TAP tests, returns to all-FREE after every flush). + */ +void +DWBFinishPageWrite(const DWBSlotRef *ref) { - return (DWBatchState) pg_atomic_read_u32(&DWBCtl->batches[batch_idx].state); + DWBReleaseSlot(ref); + + if (dwb_retire_workers == 0 || !IsUnderPostmaster) + (void) DWBRetireAllSync(); } /* ---------------------------------------------------------------- - * process exit cleanup + * abort / process exit cleanup * ---------------------------------------------------------------- */ /* - * Runs strictly on shmem DWB state: by the time on_proc_exit callbacks run, - * LWLockReleaseAll has already dropped any content locks (ipc.c) and the - * private page copy died with the process, so the staged copy in the batch - * is the authoritative source for our slots. + * Repair the data page of an abandoned ref from the batch file. + * + * If the writer died out of a failed smgrwrite (step 6), the data page may + * be torn on disk while the batch — and with it the only whole copy — is + * about to retire and recycle. Overwriting the page with the durable batch + * copy makes the disk page whole again; the shared buffer is still dirty + * (the abort path never clears BM_DIRTY), so newer content still reaches + * the disk through a later flush. + * + * This runs from the ResourceOwner release, BEFORE the buffer-IO cleanup: + * BM_IO_IN_PROGRESS of the failed flush is still ours, so no concurrent + * flush of the same page can be in flight and writing the (possibly stale) + * batch copy cannot overwrite a newer image. For the same reason the + * relation cannot be dropped or truncated under us — both invalidate the + * buffer first and that waits for our IO flag — so the ENOENT/short-file + * exits are pure defense (and serve test refs pointing at fake relations). + * + * Durability: our segment is in the batch's seg_set, and the seg_set is + * published only after every ref (ours included) is gone, so retirement + * fsyncs this segment strictly after this write. */ static void -DWBProcExit(int code, Datum arg) +DWBRewriteAbandonedSlot(const DWBSlotRef *ref) { - while (nPendingRefs > 0) + DWBatchCtl *batch = &DWBCtl->batches[ref->batch_idx]; + BufferTag tag = batch->pages[ref->slot_idx]; + uint32 segno = tag.blockNum / ((BlockNumber) RELSEG_SIZE); + PGAlignedBlock image; + RelPathStr relpath; + char path[MAXPGPATH]; + int fd; + struct stat st; + off_t off; + ssize_t written; + + DWBReadSlotImage(ref->batch_idx, ref->slot_idx, image.data); + + relpath = relpathperm(BufTagGetRelFileLocator(&tag), + BufTagGetForkNum(&tag)); + if (segno == 0) + snprintf(path, MAXPGPATH, "%s", relpath.str); + else + snprintf(path, MAXPGPATH, "%s.%u", relpath.str, segno); + + fd = OpenTransientFile(path, O_RDWR | PG_BINARY); + if (fd < 0) { - DWBSlotRef ref = pendingRefs[--nPendingRefs]; - DWBatchCtl *batch = &DWBCtl->batches[ref.batch_idx]; - uint64 bit = UINT64CONST(1) << (ref.slot_idx % 64); - pg_atomic_uint64 *word = - &batch->slots_written_bitmap[ref.slot_idx / 64]; + if (errno == ENOENT) + return; /* relation dropped: the write is moot */ + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not open file \"%s\" to repair an abandoned double write buffer slot: %m", + path))); + } - if (!(pg_atomic_read_u64(word) & bit)) - { - /* copy never published: poison the slot so the seal-waiter - * wakes up and recovery ignores it */ - batch->slot_flags[ref.slot_idx] |= DWB_SLOT_ABORTED; - pg_write_barrier(); - pg_atomic_fetch_or_u64(word, bit); - ConditionVariableBroadcast(&batch->cv_state); - } - else - { - /* copy published but smgrwrite may not have happened: hand the - * write over to the retire worker (Stage 2 completes orphans) */ - uint32 n = pg_atomic_fetch_add_u32(&batch->orphaned_refs_count, 1); + off = (off_t) (tag.blockNum % ((BlockNumber) RELSEG_SIZE)) * BLCKSZ; + if (fstat(fd, &st) < 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not stat file \"%s\": %m", path))); + if (off + BLCKSZ > st.st_size) + { + /* segment truncated: the write is moot */ + CloseTransientFile(fd); + return; + } - batch->orphan_tags[n] = batch->pages[ref.slot_idx]; - } + errno = 0; + written = pg_pwrite(fd, image.data, BLCKSZ, off); + if (written != BLCKSZ) + { + if (errno == 0) + errno = ENOSPC; + /* the page may now be torn with its DWB copy about to recycle */ + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not repair block %u of file \"%s\" from the double write buffer: %m", + tag.blockNum, path))); + } + + if (CloseTransientFile(fd) != 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", path))); +} +/* + * Release a ref whose owner is going away without the normal + * DWBReleaseSlot: transaction abort (ResourceOwner) or process exit. No + * ResourceOwnerForget here — the ResourceOwner path removes the resource + * implicitly, and at process exit the owner dies with the process. Works + * strictly on shmem DWB state plus the batch file — the private page copy + * of the writer is gone. + */ +static void +DWBAbandonRef(DWBPendingRef *pref) +{ + DWBSlotRef ref = pref->ref; + DWBatchCtl *batch = &DWBCtl->batches[ref.batch_idx]; + uint64 bit = UINT64CONST(1) << (ref.slot_idx % 64); + pg_atomic_uint64 *word = + &batch->slots_written_bitmap[ref.slot_idx / 64]; + + pref->owner = NULL; + pref->in_use = false; + nPendingRefs--; + + /* a held ref pins the batch, so its incarnation cannot have changed */ + Assert(ref.batch_id == batch->batch_id); + + if (!(pg_atomic_read_u64(word) & bit)) + { /* - * The last ref finishes the batch only once it is FSYNCED. A - * sealed batch cannot lose its last ref earlier — the leader holds - * its own pin from SEAL to FSYNCED (see DWBSealBatch) — so reaching - * zero refs in an earlier state means the batch is not sealed yet: - * it stays open and a later seal completes it normally. + * Copy never published: poison the slot so the seal-waiter wakes + * up and recovery ignores it. */ - if (pg_atomic_fetch_sub_u32(&batch->ref_count, 1) == 1 && - pg_atomic_read_u32(&batch->state) == DWB_FSYNCED) - DWBFinishBatchData(batch); + batch->slot_flags[ref.slot_idx] |= DWB_SLOT_ABORTED; + pg_write_barrier(); + pg_atomic_fetch_or_u64(word, bit); + ConditionVariableBroadcast(&batch->cv_state); + } + else if (pg_atomic_read_u32(&batch->state) >= DWB_FSYNCED) + { + /* + * Copy published and the batch is durable, which means the writer + * was at or past step 6: its smgrwrite may have failed halfway. + * Make the data page whole again from the batch copy. + */ + DWBRewriteAbandonedSlot(&ref); + } + + /* + * The last ref finishes the batch only once it is FSYNCED. A sealed + * batch cannot lose its last ref earlier — the leader holds its own + * pin from SEAL to FSYNCED (see DWBSealBatch) — so reaching zero refs + * in an earlier state means the batch is not sealed yet: it stays open + * and a later seal completes it normally. + */ + if (pg_atomic_fetch_sub_u32(&batch->ref_count, 1) == 1 && + pg_atomic_read_u32(&batch->state) == DWB_FSYNCED) + DWBFinishBatchData(batch); +} + +/* + * ResourceOwner release of one ref: the abort path of the write path. + */ +static void +ResOwnerReleaseDWBRef(Datum res) +{ + DWBAbandonRef((DWBPendingRef *) DatumGetPointer(res)); +} + +/* + * Process-exit backstop for refs that no ResourceOwner released (test refs + * acquired without an owner; anything a nonstandard exit path missed). By + * this time LWLockReleaseAll has already dropped any content locks (ipc.c) + * and buffer-IO flags may be gone too, so unlike the ResourceOwner path + * the abandoned-slot rewrite here is best effort against concurrent + * flushes; the primary cleanup is the ResourceOwner one. + */ +static void +DWBProcExit(int code, Datum arg) +{ + for (int i = 0; i < (int) lengthof(pendingRefs); i++) + { + if (pendingRefs[i].in_use) + DWBAbandonRef(&pendingRefs[i]); } } diff --git a/src/backend/storage/dwb/dwb_ctl.c b/src/backend/storage/dwb/dwb_ctl.c index d9dfb8e63e760..1c68dfd6f6c6a 100644 --- a/src/backend/storage/dwb/dwb_ctl.c +++ b/src/backend/storage/dwb/dwb_ctl.c @@ -35,6 +35,7 @@ int dwb_on_stall = DWB_ON_STALL_PANIC; DWCtl *DWBCtl = NULL; char *DWBStagingBase = NULL; +HTAB *DWSegmentHash = NULL; static Size DWBCtlSize(void) @@ -43,6 +44,13 @@ DWBCtlSize(void) mul_size(dwb_num_batches, sizeof(DWBatchCtl)); } +static Size +DWBSegEntrySize(void) +{ + return offsetof(DWSegEntry, batch_bitmap) + + mul_size(DWBSegBitmapWords(), sizeof(pg_atomic_uint64)); +} + static Size DWBStagingSize(void) { @@ -55,10 +63,15 @@ DWBStagingSize(void) Size DWBShmemSize(void) { + Size size; + if (!DWBIsEnabled()) return 0; - return add_size(DWBCtlSize(), DWBStagingSize()); + size = add_size(DWBCtlSize(), DWBStagingSize()); + size = add_size(size, hash_estimate_size(dwb_max_segments, + DWBSegEntrySize())); + return size; } void @@ -78,6 +91,7 @@ DWBShmemInit(void) for (int i = 0; i < DWB_NUM_WCLASSES; i++) pg_atomic_init_u32(&DWBCtl->open_batch_idx[i], DWB_INVALID_BATCH); pg_atomic_init_u64(&DWBCtl->next_batch_id, 1); + pg_atomic_init_u64(&DWBCtl->freed_events, 0); ConditionVariableInit(&DWBCtl->cv_free_batch); ConditionVariableInit(&DWBCtl->cv_retire_wake); SpinLockInit(&DWBCtl->staging_lock); @@ -94,7 +108,6 @@ DWBShmemInit(void) pg_atomic_init_u64(&batch->slots_written_bitmap[w], 0); pg_atomic_init_u32(&batch->ref_count, 0); pg_atomic_init_u32(&batch->seg_pending_count, 0); - pg_atomic_init_u32(&batch->orphaned_refs_count, 0); LWLockInitialize(&batch->publish_lock, LWTRANCHE_DWB_PUBLISH); ConditionVariableInit(&batch->cv_state); batch->staging_idx = -1; @@ -108,4 +121,16 @@ DWBShmemInit(void) &found); DWBStagingBase = (char *) TYPEALIGN(PG_IO_ALIGN_SIZE, base); } + + { + HASHCTL info; + + info.keysize = sizeof(DWSegRef); + info.entrysize = DWBSegEntrySize(); + + DWSegmentHash = ShmemInitHash("DWB Segment Hash", + dwb_max_segments, dwb_max_segments, + &info, + HASH_ELEM | HASH_BLOBS | HASH_FIXED_SIZE); + } } diff --git a/src/backend/storage/dwb/dwb_file.c b/src/backend/storage/dwb/dwb_file.c index 74359d08e24c2..8647d336420f0 100644 --- a/src/backend/storage/dwb/dwb_file.c +++ b/src/backend/storage/dwb/dwb_file.c @@ -23,6 +23,8 @@ #include "common/file_utils.h" #include "miscadmin.h" +#include "pgstat.h" +#include "storage/bufmgr.h" #include "storage/dwb.h" #include "storage/fd.h" #include "utils/memutils.h" @@ -291,6 +293,36 @@ DWBPrepareBatchWrite(int batch_idx) (void) DWBOpenBatchFile(batch_idx); } +/* + * Read one slot's page image back from a batch file. Used by the abort + * cleanup of a published ref whose batch is already durable (>= FSYNCED): + * the staged copy in shmem is gone by then, the batch file is the + * authoritative source. Failure is PANIC — the caller is about to repair a + * possibly-torn data page and has no fallback. + */ +void +DWBReadSlotImage(int batch_idx, int slot_idx, char *dst) +{ + File file = DWBOpenBatchFile(batch_idx); + off_t off = DWBMetaRegionSize(dwb_batch_pages) + + (off_t) slot_idx * BLCKSZ; + ssize_t r; + + r = FileRead(file, dst, BLCKSZ, off, WAIT_EVENT_DWB_BATCH_READ); + if (r != BLCKSZ) + { + if (r < 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not read slot %d of batch %d in \"%s\": %m", + slot_idx, batch_idx, DWB_DIR))); + ereport(PANIC, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("could not read slot %d of batch %d in \"%s\": read %zd of %d", + slot_idx, batch_idx, DWB_DIR, r, BLCKSZ))); + } +} + /* * Leader write of one batch: (a) one contiguous pwrite of the image stream * from staging, (b) one pwrite of the meta region, (c) fdatasync. Exactly @@ -311,10 +343,13 @@ DWBWriteBatch(int batch_idx, const DWBBatchHeader *hdr, Size image_bytes = (Size) hdr->n_slots * BLCKSZ; ssize_t nwritten; int fd; + instr_time io_start; /* DWBPrepareBatchWrite has run */ Assert(meta_buf != NULL); + io_start = pgstat_prepare_io_time(track_io_timing); + nwritten = FileWrite(file, images, image_bytes, meta_region, WAIT_EVENT_DWB_BATCH_WRITE); if (nwritten != (ssize_t) image_bytes) @@ -336,10 +371,14 @@ DWBWriteBatch(int batch_idx, const DWBBatchHeader *hdr, errmsg("could not write batch %d of \"%s\": %m", batch_idx, DWB_DIR))); + pgstat_count_io_op_time(IOOBJECT_DWB, IOCONTEXT_NORMAL, IOOP_WRITE, + io_start, 1, image_bytes + meta_region); + /* * fdatasync suffices: the file was fully preallocated at ring creation, * its size and block layout never change (WAL-segment contract). */ + io_start = pgstat_prepare_io_time(track_io_timing); pgstat_report_wait_start(WAIT_EVENT_DWB_BATCH_SYNC); fd = FileGetRawDesc(file); if (fd < 0 || pg_fdatasync(fd) != 0) @@ -348,4 +387,6 @@ DWBWriteBatch(int batch_idx, const DWBBatchHeader *hdr, errmsg("could not fsync batch %d of \"%s\": %m", batch_idx, DWB_DIR))); pgstat_report_wait_end(); + pgstat_count_io_op_time(IOOBJECT_DWB, IOCONTEXT_NORMAL, IOOP_FSYNC, + io_start, 1, 0); } diff --git a/src/backend/storage/dwb/dwb_retire.c b/src/backend/storage/dwb/dwb_retire.c new file mode 100644 index 0000000000000..6f4ac163a6b12 --- /dev/null +++ b/src/backend/storage/dwb/dwb_retire.c @@ -0,0 +1,675 @@ +/*------------------------------------------------------------------------- + * + * dwb_retire.c + * Retirement of double write buffer batches: the segment->batch + * back-reference hash, the durability accounting that frees batches, + * and the retire worker pool. + * + * A batch reaches FREE only after every segment in its seg_set has been + * fsynced after the batch's data-file writes. The accounting protocol + * (3.5 of the design plan): + * + * - publication: at DATA_WRITTEN -> RETIRING, under the batch's + * publish_lock, seg_pending_count := n_segs and one bit per segment is + * set in the segment's DWSegmentHash entry; + * - decrement: a successful segment fsync clears the bits that were + * already set BEFORE the fsync started (a bit published mid-fsync may + * cover a write the fsync missed) and decrements seg_pending_count of + * the owning batches; the decrement to zero frees the batch; + * - ABA guard: bits address batches by ring index, which is reused, so + * the fsyncer snapshots (batch_idx, batch_id) before the fsync and, + * under that batch's publish_lock, decrements only if batch_id still + * matches. + * + * Fsyncs come from three independent sources: the retire worker pool + * (proactive, partitioned by segment hash), ProcessSyncRequests in the + * checkpointer (opportunistic, wrapped by DWBSegmentFsyncBegin/End), and + * writers stuck on a full ring helping themselves (DWBRetireAllSync). + * All of them share this accounting; duplicate fsyncs are wasted work at + * worst, never a correctness problem. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/storage/dwb/dwb_retire.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "common/hashfn.h" +#include "miscadmin.h" +#include "port/pg_bitutils.h" +#include "postmaster/bgworker.h" +#include "postmaster/interrupt.h" +#include "storage/dwb.h" +#include "storage/fd.h" +#include "storage/ipc.h" +#include "storage/lwlock.h" +#include "storage/md.h" +#include "storage/sync.h" +#include "utils/guc.h" +#include "utils/timestamp.h" +#include "utils/wait_event.h" + +static void DWBMaybeRemoveSegEntry(DWSegEntry *entry); +static int DWBRetireSweep(int worker_id); + +/* + * Snapshot of one segment's back-references, taken before an fsync of that + * segment and consumed after it. One at a time per process: a process + * fsyncs one segment at a time on every path that uses this. + */ +typedef struct DWBSegSyncSnap +{ + bool active; + DWSegRef seg; + int npairs; + struct + { + int batch_idx; + uint64 batch_id; + } pairs[1024]; /* dwb_num_batches max (GUC) */ +} DWBSegSyncSnap; + +static DWBSegSyncSnap seg_sync_snap; + +static DWSegRef +DWBSegRefFromFileTag(const FileTag *ftag) +{ + DWSegRef seg; + + memset(&seg, 0, sizeof(seg)); /* keyed by memcmp: no padding garbage */ + seg.rlocator = ftag->rlocator; + seg.forknum = (ForkNumber) ftag->forknum; + seg.segno = (uint32) ftag->segno; + return seg; +} + +static FileTag +DWBFileTagFromSegRef(const DWSegRef *seg) +{ + FileTag tag; + + memset(&tag, 0, sizeof(tag)); + tag.handler = SYNC_HANDLER_MD; + tag.forknum = (int16) seg->forknum; + tag.rlocator = seg->rlocator; + tag.segno = seg->segno; + return tag; +} + +/* ---------------------------------------------------------------- + * publication + * ---------------------------------------------------------------- + */ + +/* + * Free a batch and wake everything that may be waiting for ring space. + * The caller has already moved the state to DWB_FREE. + */ +static void +DWBNoteBatchFreed(void) +{ + pg_atomic_fetch_add_u64(&DWBCtl->freed_events, 1); + ConditionVariableBroadcast(&DWBCtl->cv_free_batch); +} + +/* + * Fsync one segment for retirement purposes, tolerating a concurrently + * dropped relation: the data-file writes of a dropped segment are moot, so + * ENOENT counts as covered. Any other failure follows the vanilla + * data_sync_retry policy (PANIC by default). + */ +static void +DWBRetireSyncSegment(const DWSegRef *seg) +{ + FileTag tag = DWBFileTagFromSegRef(seg); + char path[MAXPGPATH]; + + if (mdsyncfiletag(&tag, path) < 0) + { + if (errno == ENOENT) + { + elog(DEBUG1, "DWB: segment \"%s\" dropped during retire, skipping fsync", + path); + return; + } + ereport(data_sync_elevel(ERROR), + (errcode_for_file_access(), + errmsg("could not fsync file \"%s\": %m", path))); + } +} + +/* + * Synchronous retire of a batch whose seg_set could not be published into a + * full DWSegmentHash: the publisher itself fsyncs every segment and frees + * the batch, so an undersized hash degrades throughput instead of wedging + * the ring. Runs with no locks held; the DWB_OOM_RETIRING state keeps + * everyone else away from the batch. + */ +static void +DWBRetireBatchSyncOOM(DWBatchCtl *batch) +{ + uint32 expected; + + for (uint32 i = 0; i < batch->n_segs; i++) + DWBRetireSyncSegment(&batch->seg_set[i]); + + expected = DWB_OOM_RETIRING; + if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, DWB_FREE)) + elog(PANIC, "DWB batch freed in unexpected state %u", expected); + DWBNoteBatchFreed(); + + ereport(WARNING, + (errmsg("double write buffer segment hash is full"), + errhint("Consider increasing \"dwb_max_segments\"."))); +} + +/* + * Publish a batch's seg_set into DWSegmentHash and hand the batch over to + * retirement (DATA_WRITTEN -> RETIRING). Called by whoever drops the last + * ref (see DWBFinishBatchData). + */ +void +DWBPublishBatchSegSet(int batch_idx) +{ + DWBatchCtl *batch = &DWBCtl->batches[batch_idx]; + uint32 published = 0; + bool oom = false; + uint32 expected; + + LWLockAcquire(&batch->publish_lock, LW_EXCLUSIVE); + + pg_atomic_write_u32(&batch->seg_pending_count, batch->n_segs); + pg_write_barrier(); + + LWLockAcquire(DWBSegHashLock, LW_EXCLUSIVE); + for (published = 0; published < batch->n_segs; published++) + { + bool found; + DWSegEntry *entry; + + entry = (DWSegEntry *) hash_search(DWSegmentHash, + &batch->seg_set[published], + HASH_ENTER_NULL, &found); + if (entry == NULL) + { + oom = true; + break; + } + if (!found) + { + pg_atomic_init_u32(&entry->fsync_in_progress, 0); + for (uint32 w = 0; w < DWBSegBitmapWords(); w++) + pg_atomic_init_u64(&entry->batch_bitmap[w], 0); + } + pg_atomic_fetch_or_u64(&entry->batch_bitmap[batch_idx / 64], + UINT64CONST(1) << (batch_idx % 64)); + } + + if (oom) + { + /* + * Take the partial publication back. Nobody saw those bits: they + * were set and are removed under one continuous exclusive hold of + * DWBSegHashLock. + */ + for (uint32 i = 0; i < published; i++) + { + bool found; + DWSegEntry *entry; + + entry = (DWSegEntry *) hash_search(DWSegmentHash, + &batch->seg_set[i], + HASH_FIND, &found); + if (entry == NULL) + continue; + pg_atomic_fetch_and_u64(&entry->batch_bitmap[batch_idx / 64], + ~(UINT64CONST(1) << (batch_idx % 64))); + DWBMaybeRemoveSegEntry(entry); + } + pg_atomic_write_u32(&batch->seg_pending_count, 0); + + expected = DWB_DATA_WRITTEN; + if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, + DWB_OOM_RETIRING)) + elog(PANIC, "DWB batch OOM-retiring in unexpected state %u", + expected); + } + LWLockRelease(DWBSegHashLock); + + if (!oom) + { + pg_write_barrier(); + expected = DWB_DATA_WRITTEN; + if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, + DWB_RETIRING)) + elog(PANIC, "DWB batch retiring in unexpected state %u", expected); + } + LWLockRelease(&batch->publish_lock); + + if (oom) + DWBRetireBatchSyncOOM(batch); + else + ConditionVariableBroadcast(&DWBCtl->cv_retire_wake); +} + +/* ---------------------------------------------------------------- + * decrement + * ---------------------------------------------------------------- + */ + +/* + * Remove a segment entry once its bitmap is empty. Caller holds + * DWBSegHashLock exclusive. Safe regardless of fsync_in_progress: nobody + * keeps entry pointers across the lock, claim holders re-look-up by key. + */ +static void +DWBMaybeRemoveSegEntry(DWSegEntry *entry) +{ + for (uint32 w = 0; w < DWBSegBitmapWords(); w++) + if (pg_atomic_read_u64(&entry->batch_bitmap[w]) != 0) + return; + if (hash_search(DWSegmentHash, &entry->key, HASH_REMOVE, NULL) == NULL) + elog(PANIC, "DWB segment hash entry vanished under exclusive lock"); +} + +/* + * Snapshot the back-references of one segment before fsyncing it. + */ +static void +DWBSegSnapBegin(const DWSegRef *seg) +{ + DWSegEntry *entry; + + /* a leftover active snapshot means the previous fsync ERROR'ed out + * between Begin and End: its bits were never cleared, just drop it */ + seg_sync_snap.active = true; + seg_sync_snap.seg = *seg; + seg_sync_snap.npairs = 0; + + LWLockAcquire(DWBSegHashLock, LW_SHARED); + entry = (DWSegEntry *) hash_search(DWSegmentHash, seg, HASH_FIND, NULL); + if (entry != NULL) + { + for (uint32 w = 0; w < DWBSegBitmapWords(); w++) + { + uint64 word = pg_atomic_read_u64(&entry->batch_bitmap[w]); + + while (word != 0) + { + int bit = pg_rightmost_one_pos64(word); + int idx = (int) (w * 64) + bit; + + word &= word - 1; + seg_sync_snap.pairs[seg_sync_snap.npairs].batch_idx = idx; + + /* + * Racy read of a 64-bit batch_id outside the publish_lock: + * a torn or stale value only makes the guarded re-check + * below skip the decrement, never decrement a wrong batch. + */ + seg_sync_snap.pairs[seg_sync_snap.npairs].batch_id = + DWBCtl->batches[idx].batch_id; + seg_sync_snap.npairs++; + } + } + } + LWLockRelease(DWBSegHashLock); +} + +/* + * Consume the snapshot after the fsync. If synced is false (the fsync did + * not happen and the segment still exists), the snapshot is discarded and + * the bits stay for a later fsyncer. Returns the number of batches this + * call moved RETIRING -> FREE. + */ +static int +DWBSegSnapEnd(bool synced) +{ + int freed = 0; + + Assert(seg_sync_snap.active); + seg_sync_snap.active = false; + + if (!synced) + return 0; + + for (int i = 0; i < seg_sync_snap.npairs; i++) + { + int idx = seg_sync_snap.pairs[i].batch_idx; + DWBatchCtl *batch = &DWBCtl->batches[idx]; + bool cleared = false; + + LWLockAcquire(&batch->publish_lock, LW_EXCLUSIVE); + if (batch->batch_id == seg_sync_snap.pairs[i].batch_id) + { + DWSegEntry *entry; + uint64 bit = UINT64CONST(1) << (idx % 64); + + LWLockAcquire(DWBSegHashLock, LW_EXCLUSIVE); + entry = (DWSegEntry *) hash_search(DWSegmentHash, + &seg_sync_snap.seg, + HASH_FIND, NULL); + if (entry != NULL) + { + uint64 prev; + + prev = pg_atomic_fetch_and_u64(&entry->batch_bitmap[idx / 64], + ~bit); + if (prev & bit) + { + cleared = true; + DWBMaybeRemoveSegEntry(entry); + } + } + LWLockRelease(DWBSegHashLock); + + if (cleared && + pg_atomic_fetch_sub_u32(&batch->seg_pending_count, 1) == 1) + { + uint32 expected = DWB_RETIRING; + + if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, + DWB_FREE)) + elog(PANIC, "DWB batch freed in unexpected state %u", + expected); + freed++; + } + } + LWLockRelease(&batch->publish_lock); + } + + if (freed > 0) + DWBNoteBatchFreed(); + + return freed; +} + +/* + * Wrap an external fsync of a relation segment (ProcessSyncRequests in the + * checkpointer). Begin before the fsync attempt; End after it, with + * synced = true if the segment was fsynced OR turned out to be dropped + * (a dropped segment's writes are moot). Non-md tags and disabled DWB are + * handled here so the caller stays a two-liner. + */ +void +DWBSegmentFsyncBegin(const FileTag *ftag) +{ + DWSegRef seg; + + if (!DWBIsEnabled() || ftag->handler != SYNC_HANDLER_MD) + return; + + seg = DWBSegRefFromFileTag(ftag); + DWBSegSnapBegin(&seg); +} + +int +DWBSegmentFsyncEnd(bool synced) +{ + if (!seg_sync_snap.active) + return 0; + return DWBSegSnapEnd(synced); +} + +/* ---------------------------------------------------------------- + * proactive retire + * ---------------------------------------------------------------- + */ + +/* + * Copy the seg_set of a batch if it is still the expected RETIRING + * incarnation. Returns the number of segments, 0 if the batch moved on. + */ +static uint32 +DWBCollectBatchSegs(int batch_idx, uint64 batch_id, DWSegRef *segs) +{ + DWBatchCtl *batch = &DWBCtl->batches[batch_idx]; + uint32 n = 0; + + LWLockAcquire(&batch->publish_lock, LW_SHARED); + if (batch->batch_id == batch_id && + pg_atomic_read_u32(&batch->state) == DWB_RETIRING) + { + n = batch->n_segs; + memcpy(segs, batch->seg_set, n * sizeof(DWSegRef)); + } + LWLockRelease(&batch->publish_lock); + return n; +} + +/* + * Fsync one segment of a RETIRING batch and decrement its back-references. + * Skips the segment when another fsyncer holds the claim (they will cover + * it) or when its bits are already gone. Returns batches freed. + */ +static int +DWBRetireSegment(const DWSegRef *seg) +{ + DWSegEntry *entry; + bool claimed = false; + uint32 zero = 0; + int freed; + + /* claim the segment; a busy or vanished entry means nothing to do */ + LWLockAcquire(DWBSegHashLock, LW_SHARED); + entry = (DWSegEntry *) hash_search(DWSegmentHash, seg, HASH_FIND, NULL); + if (entry != NULL) + claimed = pg_atomic_compare_exchange_u32(&entry->fsync_in_progress, + &zero, 1); + LWLockRelease(DWBSegHashLock); + if (!claimed) + return 0; + + DWBSegSnapBegin(seg); + DWBRetireSyncSegment(seg); + freed = DWBSegSnapEnd(true); + + /* + * Release the claim. The entry may have been removed (and even + * re-created for a new batch) meanwhile; re-look-up by key and reset + * whatever is there -- the flag is advisory, an over-reset only costs a + * duplicate fsync. + */ + LWLockAcquire(DWBSegHashLock, LW_SHARED); + entry = (DWSegEntry *) hash_search(DWSegmentHash, seg, HASH_FIND, NULL); + if (entry != NULL) + pg_atomic_write_u32(&entry->fsync_in_progress, 0); + LWLockRelease(DWBSegHashLock); + + return freed; +} + +/* + * One retire sweep over all RETIRING batches, oldest first. worker_id >= 0 + * restricts the sweep to that worker's segment partition; -1 sweeps + * everything (self-help of a writer stuck on a full ring, and the second + * retire point in ProcessSyncRequests-less paths). Returns batches freed. + */ +int +DWBRetireAllSync(void) +{ + return DWBRetireSweep(-1); +} + +static int +DWBRetireSweep(int worker_id) +{ + struct + { + int idx; + uint64 id; + } *retiring; + int nretiring = 0; + int freed = 0; + DWSegRef *segs; + + retiring = palloc(dwb_num_batches * sizeof(*retiring)); + segs = palloc(dwb_batch_pages * sizeof(DWSegRef)); + + for (int i = 0; i < dwb_num_batches; i++) + { + if (pg_atomic_read_u32(&DWBCtl->batches[i].state) == DWB_RETIRING) + { + retiring[nretiring].idx = i; + retiring[nretiring].id = DWBCtl->batches[i].batch_id; + nretiring++; + } + } + + /* oldest first: smaller batch_id was opened earlier */ + for (int i = 0; i < nretiring; i++) + for (int j = i + 1; j < nretiring; j++) + if (retiring[j].id < retiring[i].id) + { + uint64 tid = retiring[i].id; + int tidx = retiring[i].idx; + + retiring[i].id = retiring[j].id; + retiring[i].idx = retiring[j].idx; + retiring[j].id = tid; + retiring[j].idx = tidx; + } + + for (int i = 0; i < nretiring; i++) + { + uint32 nsegs = DWBCollectBatchSegs(retiring[i].idx, + retiring[i].id, segs); + + for (uint32 s = 0; s < nsegs; s++) + { + if (worker_id >= 0 && + (int) (hash_bytes((const unsigned char *) &segs[s], + sizeof(DWSegRef)) % + (uint32) dwb_retire_workers) != worker_id) + continue; + freed += DWBRetireSegment(&segs[s]); + } + } + + pfree(retiring); + pfree(segs); + return freed; +} + +/* ---------------------------------------------------------------- + * retire worker pool + * ---------------------------------------------------------------- + */ + +/* + * Register dwb_retire_workers static background workers. Called from + * PostmasterMain before extensions get a chance at the worker slots. + */ +void +DWBRetireWorkersRegister(void) +{ + BackgroundWorker bgw; + + if (!DWBIsEnabled() || dwb_retire_workers == 0) + return; + + if (dwb_retire_workers > max_worker_processes) + ereport(FATAL, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"dwb_retire_workers\" (%d) must not exceed \"max_worker_processes\" (%d)", + dwb_retire_workers, max_worker_processes))); + + for (int i = 0; i < dwb_retire_workers; i++) + { + memset(&bgw, 0, sizeof(bgw)); + + /* + * The database-less connection gives the worker a pg_stat_activity + * entry; it forces BgWorkerStart_ConsistentState, so during the + * pre-consistency part of recovery the write path relies on its + * built-in self service (waiters seal on timeout, ring-full writers + * retire inline) — the pool is throughput, not correctness. + */ + bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | + BGWORKER_BACKEND_DATABASE_CONNECTION; + bgw.bgw_start_time = BgWorkerStart_ConsistentState; + snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres"); + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DWBRetireWorkerMain"); + snprintf(bgw.bgw_name, BGW_MAXLEN, "dwb retire worker %d", i); + snprintf(bgw.bgw_type, BGW_MAXLEN, "dwb retire worker"); + bgw.bgw_restart_time = 1; + bgw.bgw_notify_pid = 0; + bgw.bgw_main_arg = Int32GetDatum(i); + + RegisterBackgroundWorker(&bgw); + } +} + +/* + * Main loop: force-SEAL non-empty batches that outlived + * dwb_batch_timeout_ms, then proactively fsync this worker's segment + * partition of every RETIRING batch. Woken by cv_retire_wake (publication + * of a seg_set, opening of a batch) or by timeout. + */ +void +DWBRetireWorkerMain(Datum main_arg) +{ + int my_id = DatumGetInt32(main_arg); + + pqsignal(SIGHUP, SignalHandlerForConfigReload); + pqsignal(SIGTERM, SignalHandlerForShutdownRequest); + BackgroundWorkerUnblockSignals(); + + /* no database, just shared state and pg_stat_activity visibility */ + BackgroundWorkerInitializeConnection(NULL, NULL, 0); + + ConditionVariablePrepareToSleep(&DWBCtl->cv_retire_wake); + + for (;;) + { + long timeout; + TimestampTz now; + + if (ShutdownRequestPending) + break; + if (ConfigReloadPending) + { + ConfigReloadPending = false; + ProcessConfigFile(PGC_SIGHUP); + } + + /* + * Force-SEAL pass: writers waiting on a half-filled batch seal it + * themselves after the same timeout, so this only matters for + * batches whose writers all went away before sealing. open_time + * is read unlocked; a torn read can only mis-time the seal, which + * is always a valid action on a non-empty ALLOCATED batch. + */ + now = GetCurrentTimestamp(); + timeout = dwb_retire_interval_ms; + for (int i = 0; i < dwb_num_batches; i++) + { + DWBatchCtl *batch = &DWBCtl->batches[i]; + long age_ms; + + if (pg_atomic_read_u32(&batch->state) != DWB_ALLOCATED) + continue; + if ((pg_atomic_read_u32(&batch->next_slot_idx) & DWB_IDX_MASK) == 0) + continue; /* empty: sealing it buys nothing */ + + age_ms = TimestampDifferenceMilliseconds(batch->open_time, now); + if (age_ms >= dwb_batch_timeout_ms) + (void) DWBTrySealBatch(i); + else if (dwb_batch_timeout_ms - age_ms < timeout) + timeout = dwb_batch_timeout_ms - age_ms; + } + + (void) DWBRetireSweep(my_id); + + (void) ConditionVariableTimedSleep(&DWBCtl->cv_retire_wake, + Max(timeout, 1), + WAIT_EVENT_DWB_RETIRE_MAIN); + } + + ConditionVariableCancelSleep(); + proc_exit(0); +} diff --git a/src/backend/storage/dwb/meson.build b/src/backend/storage/dwb/meson.build index e0a4ac73f2a1b..cc84a0fa8316b 100644 --- a/src/backend/storage/dwb/meson.build +++ b/src/backend/storage/dwb/meson.build @@ -5,4 +5,5 @@ backend_sources += files( 'dwb_ctl.c', 'dwb_file.c', 'dwb_recovery.c', + 'dwb_retire.c', ) diff --git a/src/backend/storage/sync/sync.c b/src/backend/storage/sync/sync.c index fc16db90133bb..3c374fdb15dde 100644 --- a/src/backend/storage/sync/sync.c +++ b/src/backend/storage/sync/sync.c @@ -26,6 +26,7 @@ #include "pgstat.h" #include "portability/instr_time.h" #include "postmaster/bgwriter.h" +#include "storage/dwb.h" #include "storage/fd.h" #include "storage/latch.h" #include "storage/md.h" @@ -407,6 +408,16 @@ ProcessSyncRequests(void) * DROP DATABASE likewise has to tell us to forget fsync requests * before it starts deletions. */ + + /* + * This fsync also retires double write buffer batches: snapshot + * the segment's DWB back-references now — bits published while + * the fsync runs may cover writes it missed — and decrement + * them once the segment is durable (or turns out dropped, which + * makes its data-file writes moot). + */ + DWBSegmentFsyncBegin(&entry->tag); + for (failures = 0; !entry->canceled; failures++) { char path[MAXPGPATH]; @@ -458,6 +469,9 @@ ProcessSyncRequests(void) AbsorbSyncRequests(); absorb_counter = FSYNCS_PER_ABSORB; /* might as well... */ } /* end retry loop */ + + /* durable or dropped either way: retire DWB references */ + (void) DWBSegmentFsyncEnd(true); } /* We are done with this entry, remove it */ diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c index 13ae57ed6498d..d513d3e240f69 100644 --- a/src/backend/utils/activity/pgstat_io.c +++ b/src/backend/utils/activity/pgstat_io.c @@ -268,6 +268,8 @@ pgstat_get_io_object_name(IOObject io_object) return "temp relation"; case IOOBJECT_WAL: return "wal"; + case IOOBJECT_DWB: + return "dwb"; } elog(ERROR, "unrecognized IOObject value: %d", io_object); @@ -418,6 +420,12 @@ pgstat_tracks_io_object(BackendType bktype, IOObject io_object, io_object == IOOBJECT_TEMP_RELATION) return false; + /* + * IO on the double write buffer ring only occurs in IOCONTEXT_NORMAL. + */ + if (io_object == IOOBJECT_DWB && io_context != IOCONTEXT_NORMAL) + return false; + /* * In core Postgres, only regular backends and WAL Sender processes * executing queries will use local buffers and operate on temporary @@ -516,6 +524,13 @@ pgstat_tracks_io_op(BackendType bktype, IOObject io_object, (io_op == IOOP_FSYNC || io_op == IOOP_WRITEBACK)) return false; + /* + * The double write buffer ring only sees batch writes and fdatasyncs. + */ + if (io_object == IOOBJECT_DWB && + !(io_op == IOOP_WRITE || io_op == IOOP_FSYNC)) + return false; + /* * Some IOOps are not valid in certain IOContexts and some IOOps are only * valid in certain contexts. diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 9a0b7d2aef71c..c7f34ba36fb02 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -57,6 +57,7 @@ BGWRITER_HIBERNATE "Waiting in background writer process, hibernating." BGWRITER_MAIN "Waiting in main loop of background writer process." CHECKPOINTER_MAIN "Waiting in main loop of checkpointer process." CHECKPOINTER_SHUTDOWN "Waiting for checkpointer process to be terminated." +DWB_RETIRE_MAIN "Waiting in main loop of a double write buffer retire worker." IO_WORKER_MAIN "Waiting in main loop of IO Worker process." LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process." LOGICAL_LAUNCHER_MAIN "Waiting in main loop of logical replication launcher process." @@ -222,6 +223,7 @@ DATA_FILE_TRUNCATE "Waiting for a relation data file to be truncated." DATA_FILE_WRITE "Waiting for a write to a relation data file." DSM_ALLOCATE "Waiting for a dynamic shared memory segment to be allocated." DSM_FILL_ZERO_WRITE "Waiting to fill a dynamic shared memory backing file with zeroes." +DWB_BATCH_READ "Waiting for a read from a double write buffer batch file." DWB_BATCH_SYNC "Waiting for a double write buffer batch file to reach durable storage." DWB_BATCH_WRITE "Waiting for a write to a double write buffer batch file." DWB_CONTROL_READ "Waiting for a read of the double write buffer control file." @@ -362,6 +364,7 @@ InjectionPoint "Waiting to read or update information related to injection point SerialControl "Waiting to read or update shared pg_serial state." AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." DWBRingOpen "Waiting to open a new double write buffer batch." +DWBSegHash "Waiting to read or update the double write buffer segment hash table." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index e7c37043799df..f48adacd236dc 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -2226,10 +2226,12 @@ struct config_int ConfigureNamesInt[] = { {"dwb_retire_workers", PGC_POSTMASTER, WAL_SETTINGS, gettext_noop("Number of double write buffer retire worker processes."), - NULL + gettext_noop("The workers consume \"max_worker_processes\" slots. " + "0 disables the pool and makes writers retire batches " + "synchronously; meant for testing only.") }, &dwb_retire_workers, - 1, 1, 32, + 1, 0, 32, NULL, NULL, NULL }, { diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 3a302c2cab022..e95a82bde8501 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -275,9 +275,10 @@ typedef enum IOObject IOOBJECT_RELATION, IOOBJECT_TEMP_RELATION, IOOBJECT_WAL, + IOOBJECT_DWB, } IOObject; -#define IOOBJECT_NUM_TYPES (IOOBJECT_WAL + 1) +#define IOOBJECT_NUM_TYPES (IOOBJECT_DWB + 1) typedef enum IOContext { diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 6beb2679c3d06..b8ce1e7942c89 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -29,6 +29,7 @@ #include "storage/lwlock.h" #include "storage/relfilelocator.h" #include "storage/s_lock.h" +#include "utils/hsearch.h" #include "utils/timestamp.h" /* GUC: io_torn_pages_protection */ @@ -71,7 +72,7 @@ extern PGDLLIMPORT int dwb_on_stall; #define DWB_BITMAP_WORDS (DWB_BATCH_MAX_PAGES / 64) /* staging pool: 2 writer classes + 2 in-flight leader writes */ #define DWB_STAGING_BUFFERS 4 -/* writer classes (3.6); Stage 1 uses only DWB_WCLASS_EVICTION */ +/* writer classes (3.6) */ #define DWB_NUM_WCLASSES 2 #define DWB_WCLASS_EVICTION 0 #define DWB_WCLASS_BACKGROUND 1 @@ -172,8 +173,8 @@ typedef enum DWBatchState DWB_FSYNCED, /* batch durable; writers do smgrwrite */ DWB_DATA_WRITTEN, /* all smgrwrite + sync requests done */ DWB_RETIRING, /* waiting for fsync of seg_set segments */ - DWB_OOM_RETIRING, /* publisher retires synchronously (Stage 2, - * 3.5) */ + DWB_OOM_RETIRING, /* publisher retires synchronously after a + * DWSegmentHash OOM (3.5) */ } DWBatchState; typedef struct DWSegRef @@ -183,6 +184,36 @@ typedef struct DWSegRef uint32 segno; } DWSegRef; +/* + * Segment -> batch back-reference (3.5): one shmem hash entry per segment + * that at least one RETIRING batch still needs fsynced. The bitmap is + * indexed by ring batch index; a bit is set exactly once per batch life, at + * the DATA_WRITTEN -> RETIRING transition, and cleared by the fsyncer that + * covered it. Batch index reuse is disambiguated by snapshotting batch_id + * before the fsync and re-checking it under the batch's publish_lock before + * decrementing (the ABA guard of 3.5). + * + * The entry size depends on dwb_num_batches, so the bitmap is a flexible + * array of DWBSegBitmapWords() words; hash lookups/inserts/removals are + * serialized by DWBSegHashLock. fsync_in_progress is a best-effort claim + * that lets concurrent fsyncers skip a segment somebody is already syncing; + * races on it are benign because the bit-clear + decrement is idempotent. + */ +typedef struct DWSegEntry +{ + DWSegRef key; + pg_atomic_uint32 fsync_in_progress; + pg_atomic_uint64 batch_bitmap[FLEXIBLE_ARRAY_MEMBER]; +} DWSegEntry; + +#define DWBSegBitmapWords() (((uint32) dwb_num_batches + 63) / 64) + +/* + * FREE batches held back from background-class opens so that a checkpoint's + * BufferSync storm can never eat the whole ring from under user evictions. + */ +#define DWB_EVICT_RESERVE Max(2, dwb_num_batches / 8) + /* * next_slot_idx encoding: 31-bit index + seal sentinel bit. */ @@ -199,18 +230,13 @@ typedef struct DWBatchCtl pg_atomic_uint32 ref_count; /* writers holding the batch from slot * reservation to smgrwrite done */ pg_atomic_uint32 seg_pending_count; /* seg_set entries not yet fsynced; - * Stage 1 sets and clears it - * wholesale, Stage 2 decrements it - * per fsynced segment */ - pg_atomic_uint32 orphaned_refs_count; /* refs whose writer aborted after - * publishing the copy but before - * smgrwrite; a retire worker - * finishes their writes (Stage 2; - * Stage 1 only records them) */ - LWLock publish_lock; /* protects n_segs/seg_set (dedup insert); - * Stage 2 also serializes seg_set - * publication and the seg_pending_count - * decrement (3.5) */ + * decremented once per covered + * segment, the decrement to zero + * frees the batch */ + LWLock publish_lock; /* protects n_segs/seg_set (dedup insert), + * serializes seg_set publication into + * DWSegmentHash and the batch_id-guarded + * seg_pending_count decrement (3.5) */ ConditionVariable cv_state; /* broadcast on state change */ uint32 n_segs; DWSegRef seg_set[DWB_BATCH_MAX_SEGS]; @@ -222,7 +248,6 @@ typedef struct DWBatchCtl * DWSlotMeta.flags */ int staging_idx; /* staging buffer; held from ALLOCATED until * the leader finishes the image pwrite */ - BufferTag orphan_tags[DWB_BATCH_MAX_PAGES]; XLogRecPtr max_page_lsn; uint64 batch_id; /* monotonic, for ordering */ TimestampTz open_time; /* FREE -> ALLOCATED instant; drives @@ -239,9 +264,11 @@ typedef struct DWCtl uint64 ring_generation; /* = control.generation after the startup * bump; constant until restart, stamped * into DWSlotMeta by the leader */ + pg_atomic_uint64 freed_events; /* monotonic count of batches that + * reached FREE; backpressure waiters + * treat a change as retire progress */ ConditionVariable cv_free_batch; /* broadcast on retire */ - ConditionVariable cv_retire_wake; /* wakes retire workers (Stage 2; no - * waiters yet) */ + ConditionVariable cv_retire_wake; /* wakes retire workers */ slock_t staging_lock; /* protects staging_free bitmap */ uint32 staging_free; /* bitmap of free staging buffers */ DWBatchCtl batches[FLEXIBLE_ARRAY_MEMBER]; /* dwb_num_batches entries */ @@ -259,29 +286,45 @@ typedef struct DWBSlotRef extern PGDLLIMPORT DWCtl *DWBCtl; extern PGDLLIMPORT char *DWBStagingBase; +extern PGDLLIMPORT HTAB *DWSegmentHash; /* dwb_ctl.c */ extern Size DWBShmemSize(void); extern void DWBShmemInit(void); -/* dwb.c — write path (Stage 1: driven by tests, not FlushBuffer yet) */ -extern void DWBAcquireSlot(const BufferTag *tag, DWBSlotRef *ref); +/* dwb.c — write path */ +extern void DWBStagePageWrite(const BufferTag *tag, const char *image, + XLogRecPtr page_lsn, DWBSlotRef *ref); +extern void DWBFinishPageWrite(const DWBSlotRef *ref); +extern bool DWBWritesPaused(void); +extern void DWBAcquireSlot(const BufferTag *tag, int wclass, + bool use_resowner, DWBSlotRef *ref); extern void DWBPublishImage(const DWBSlotRef *ref, const char *image, XLogRecPtr page_lsn); extern void DWBWaitBatchFsynced(const DWBSlotRef *ref); extern void DWBReleaseSlot(const DWBSlotRef *ref); extern bool DWBForceSealOpenBatch(int wclass); -extern int DWBRetireAllSync(void); +extern bool DWBTrySealBatch(int batch_idx); extern DWBatchState DWBGetBatchState(int batch_idx); /* internal; exported for test_dwb's stale-open regression test */ extern void DWBOpenNewBatch(int wclass, uint32 old_idx); +/* dwb_retire.c — segment hash, retirement, worker pool */ +struct FileTag; /* avoid dragging storage/sync.h in here */ +extern void DWBPublishBatchSegSet(int batch_idx); +extern void DWBSegmentFsyncBegin(const struct FileTag *ftag); +extern int DWBSegmentFsyncEnd(bool synced); +extern int DWBRetireAllSync(void); +extern void DWBRetireWorkersRegister(void); +pg_noreturn extern void DWBRetireWorkerMain(Datum main_arg); + /* dwb_file.c */ extern void DWBCreateRing(void); extern bool DWBReadControlFile(DWBControlFileData *control, bool missing_ok); extern void DWBWriteControlFile(const DWBControlFileData *control); extern int DWBOpenBatchFile(int batch_idx); extern void DWBPrepareBatchWrite(int batch_idx); +extern void DWBReadSlotImage(int batch_idx, int slot_idx, char *dst); extern void DWBWriteBatch(int batch_idx, const DWBBatchHeader *hdr, const DWSlotMeta *metas, const char *images); extern pg_crc32c DWBImageCrc(const char *image); diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 671e84e4c24ee..3af3f4ad56a32 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -85,3 +85,4 @@ PG_LWLOCK(51, InjectionPoint) PG_LWLOCK(52, SerialControl) PG_LWLOCK(53, AioWorkerSubmissionQueue) PG_LWLOCK(54, DWBRingOpen) +PG_LWLOCK(55, DWBSegHash) diff --git a/src/test/modules/test_dwb/Makefile b/src/test/modules/test_dwb/Makefile index e2234553e094d..24a913ea10763 100644 --- a/src/test/modules/test_dwb/Makefile +++ b/src/test/modules/test_dwb/Makefile @@ -11,6 +11,10 @@ TAP_TESTS = 1 EXTENSION = test_dwb DATA = test_dwb--1.0.sql +# 003_backpressure.pl uses the injection_points extension +EXTRA_INSTALL = src/test/modules/injection_points +export enable_injection_points + REGRESS_OPTS = --temp-config $(top_srcdir)/src/test/modules/test_dwb/test_dwb.conf REGRESS = test_dwb # Disabled because these tests require io_torn_pages_protection=double_writes, diff --git a/src/test/modules/test_dwb/t/001_dwb.pl b/src/test/modules/test_dwb/t/001_dwb.pl index cd4540085998d..8448b74c8c3c2 100644 --- a/src/test/modules/test_dwb/t/001_dwb.pl +++ b/src/test/modules/test_dwb/t/001_dwb.pl @@ -12,11 +12,18 @@ my $node = PostgreSQL::Test::Cluster->new('dwb'); $node->init; +# dwb_retire_workers = 0 keeps batch sealing and retirement fully under the +# test's control (writers retire synchronously); the quiescing settings keep +# background flushes from opening batches between the state assertions. $node->append_conf( 'postgresql.conf', qq( io_torn_pages_protection = double_writes dwb_num_batches = 16 dwb_batch_pages = 16 +dwb_retire_workers = 0 +bgwriter_lru_maxpages = 0 +checkpoint_timeout = 1h +autovacuum = off )); $node->start; $node->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); @@ -80,13 +87,18 @@ sub flip_byte # --- restart bumps the durable generation ------------------------------ +# The shutdown checkpoint itself streams pages through the ring, so exact +# slot counts cannot survive a restart; the invariants that must hold are +# that CRC-valid slots exist and that none of them belongs to the new +# generation. my $stale = $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(false)'); cmp_ok($stale, '>', 0, 'ring holds slots before the restart check'); $node->restart; is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), '0', 'no slot belongs to the new generation after restart'); -is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(false)'), - $stale, 'stale slots still CRC-valid, only the generation gates them'); +cmp_ok( + $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(false)'), + '>', 0, 'stale slots still CRC-valid, only the generation gates them'); # --- process exit cleanup ---------------------------------------------- @@ -130,9 +142,37 @@ sub flip_byte $node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=16/, 'ring fully idle after the orphan hand-off'); -# --- stale open must not hijack a reopened index ------------------------ +# --- transaction abort releases refs (ResourceOwner path) --------------- +# An ERROR with unpublished refs: the abort poisons the slots, and the +# batch seals and completes later exactly like the dead-backend case — +# without a process exit. my ($rc, $out, $err) = + $node->psql('postgres', 'SELECT test_dwb_abort_release(3, false)'); +isnt($rc, 0, 'deliberate abort with pending refs reported'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/allocated=1/, 'batch of the aborted transaction stays open'); +is( $node->safe_psql('postgres', 'SELECT test_dwb_force_seal()'), + 't', 'batch of the aborted transaction seals'); +is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), + '1', 'batch of the aborted transaction retires'); + +# An ERROR after the batch is durable: the abort cleanup goes through the +# abandoned-slot repair (the fake relation exits via the dropped-relation +# branch) and must still hand the batch over to retirement. +($rc, $out, $err) = + $node->psql('postgres', 'SELECT test_dwb_abort_after_fsync()'); +isnt($rc, 0, 'deliberate abort after batch fsync reported'); +is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), + '1', 'batch of the post-fsync abort retires'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/free=16/, 'ring idle after the abort scenarios'); + +# --- stale open must not hijack a reopened index ------------------------ + +($rc, $out, $err) = $node->psql('postgres', 'SELECT test_dwb_open_stale()'); is($rc, 0, 'stale open leaves the live reopened batch alone') or diag($err); diff --git a/src/test/modules/test_dwb/t/002_flushbuffer.pl b/src/test/modules/test_dwb/t/002_flushbuffer.pl new file mode 100644 index 0000000000000..ebac14fa4b712 --- /dev/null +++ b/src/test/modules/test_dwb/t/002_flushbuffer.pl @@ -0,0 +1,82 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# End-to-end tests of the FlushBuffer integration: real pages flow through +# the double write buffer, the retire worker pool frees the ring, pg_stat_io +# accounts the batch IO, and the cluster survives a crash. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_flush'); +$node->init; +# A tiny buffer pool forces evictions through the DWB on a modest workload; +# the short batch timeout keeps the worker pool sealing and retiring briskly. +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 2 +dwb_batch_timeout_ms = 20 +shared_buffers = 2MB +autovacuum = off +)); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); + +# --- the worker pool is running ----------------------------------------- + +# the workers start asynchronously once the server is up +$node->poll_query_until('postgres', + "SELECT count(*) = 2 FROM pg_stat_activity WHERE backend_type = 'dwb retire worker'") + or die 'timed out waiting for the retire workers to start'; +pass('both retire workers are running'); + +# --- a real workload flows through the ring ------------------------------ + +$node->safe_psql('postgres', q( + CREATE TABLE dwb_t AS + SELECT g AS id, repeat('x', 300) AS filler + FROM generate_series(1, 50000) g; + UPDATE dwb_t SET filler = repeat('y', 300) WHERE id % 10 = 0; +)); +$node->safe_psql('postgres', 'CHECKPOINT'); + +is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), + '50000', 'workload survived the DWB write path'); + +# The workload far exceeds shared_buffers, so evictions must have staged +# real pages into the ring under the current generation. +cmp_ok( + $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), + '>', 0, 'real pages were staged into the ring'); + +# --- the worker pool retires everything ---------------------------------- + +$node->poll_query_until('postgres', + "SELECT test_dwb_states() LIKE 'free=16 %'") + or die 'timed out waiting for the retire workers to free the ring'; +pass('retire workers returned the ring to all-free'); + +# --- pg_stat_io accounts the batch writes and fdatasyncs ----------------- + +is( $node->safe_psql( + 'postgres', + "SELECT sum(writes) > 0 AND sum(fsyncs) > 0 FROM pg_stat_io WHERE object = 'dwb'"), + 't', 'pg_stat_io shows double write buffer writes and fsyncs'); + +# --- crash recovery: data intact, generation bumped ---------------------- + +$node->safe_psql('postgres', + "UPDATE dwb_t SET filler = repeat('z', 300) WHERE id % 7 = 0"); +$node->stop('immediate'); +$node->start; + +is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), + '50000', 'data intact after crash recovery'); + +done_testing(); diff --git a/src/test/modules/test_dwb/t/003_backpressure.pl b/src/test/modules/test_dwb/t/003_backpressure.pl new file mode 100644 index 0000000000000..24ebd5e55766b --- /dev/null +++ b/src/test/modules/test_dwb/t/003_backpressure.pl @@ -0,0 +1,126 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Stage B backpressure policy tests: with the ring exhausted, a stalled +# non-critical writer gets an ERROR (dwb_on_stall = error) and the cluster +# stays up, while a stalled checkpointer always PANICs by role policy and +# the cluster crash-recovers. The dwb-force-stall injection point makes +# the current wait escalate immediately instead of after +# dwb_write_timeout_ms; everything else is the production code path. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if (!defined $ENV{enable_injection_points} + || $ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'injection points not supported by this build'; +} + +my $node = PostgreSQL::Test::Cluster->new('dwb_stall'); +$node->init; +# No retire workers and no background flushers: the ring stays exactly as +# exhausted as test_dwb_fill_ring() leaves it. shared_buffers is sized so +# that ONLY the deliberately oversized victim workload evicts dirty pages — +# incidental sessions (attach/detach, liveness probes) must never touch the +# exhausted ring, or they would stall in its place. +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 0 +dwb_on_stall = error +bgwriter_lru_maxpages = 0 +checkpoint_timeout = 1h +autovacuum = off +shared_buffers = 16MB +restart_after_crash = on +)); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points'); + +# Dirty pages for the checkpointer scenario, created while the ring is +# still healthy and small enough to stay in shared_buffers. +$node->safe_psql('postgres', q( + CREATE TABLE dwb_dirty AS + SELECT g AS id, repeat('d', 300) AS filler + FROM generate_series(1, 1000) g; +)); + +# --- ERROR in a non-critical writer keeps the cluster alive -------------- + +# Attach while the ring is still healthy; the point only fires for a +# process already stuck waiting for ring space. +$node->safe_psql('postgres', + "SELECT injection_points_attach('dwb-force-stall', 'notice')"); + +my $filler = $node->background_psql('postgres'); +my $taken = $filler->query_safe('SELECT test_dwb_fill_ring()'); +cmp_ok($taken, '>', 0, 'ring exhausted by leaked refs'); + +# The victim outgrows shared_buffers, so it must evict its own dirty pages +# through the exhausted ring. Its rollback drops its buffers unwritten, +# leaving the pool clean for the sessions that follow. +my ($rc, $out, $err) = $node->psql('postgres', q( + CREATE TABLE dwb_victim AS + SELECT g AS id, repeat('v', 300) AS filler + FROM generate_series(1, 80000) g; +)); +isnt($rc, 0, 'stalled eviction fails instead of hanging'); +like( + $err, + qr/double write buffer retirement made no progress/, + 'stall ERROR reported to the writer'); + +is( $node->safe_psql('postgres', 'SELECT 1'), + '1', 'cluster alive after the writer ERROR'); + +$node->safe_psql('postgres', + "SELECT injection_points_detach('dwb-force-stall')"); + +# Releasing the leaked refs lets the abandoned batches finish. With no +# worker pool, drive sealing and retirement from the poll itself (nested +# CASEs order the side effects before the state probe). +$filler->quit; +$node->poll_query_until('postgres', + "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " + . "CASE WHEN test_dwb_retire() >= 0 THEN " + . "test_dwb_states() LIKE 'free=16 %' END END") + or die 'timed out waiting for the ring to drain after the ERROR scenario'; + +# --- a stalled checkpointer PANICs by role policy ------------------------- + +# Dirty the pages for BufferSync while the ring is still healthy, THEN +# exhaust it: the UPDATE itself must not stall. +$node->safe_psql('postgres', + "UPDATE dwb_dirty SET filler = repeat('e', 300) WHERE id % 2 = 0"); + +$filler = $node->background_psql('postgres'); +$taken = $filler->query_safe('SELECT test_dwb_fill_ring()'); +cmp_ok($taken, '>', 0, 'ring exhausted again for the checkpointer scenario'); + +$node->safe_psql('postgres', + "SELECT injection_points_attach('dwb-force-stall', 'notice')"); + +my $log_offset = -s $node->logfile; +($rc, $out, $err) = $node->psql('postgres', 'CHECKPOINT'); +isnt($rc, 0, 'CHECKPOINT fails when the checkpointer PANICs'); + +# probing with psql during the restart window trips over dying sockets; +# wait for the crash-recovery cycle in the log instead +$node->wait_for_log(qr/database system is ready to accept connections/, + $log_offset); +pass('cluster restarted after the checkpointer PANIC'); +ok( $node->log_contains( + 'double write buffer retirement made no progress', $log_offset), + 'checkpointer stall escalated to the role-policy PANIC'); + +is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_dirty'), + '1000', 'data intact after crash recovery'); + +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index 12c5e3bc05507..aefb1a5618592 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -34,3 +34,15 @@ CREATE FUNCTION test_dwb_retire() CREATE FUNCTION test_dwb_open_stale() RETURNS void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_fill_ring() + RETURNS int STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_abort_release(npages int, do_publish bool) + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_abort_after_fsync() + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index b62cff325575a..0f64c303279b6 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -3,9 +3,9 @@ * test_dwb.c * Test module for the short-lived double write buffer. * - * Drives the DWB batch state machine directly (Stage 1: FlushBuffer is not - * wired in yet) with synthetic page tags and images, and validates the - * on-disk ring format independently of the server-side write path. + * Drives the DWB batch state machine directly with synthetic page tags and + * images — independently of the FlushBuffer integration — and validates + * the on-disk ring format independently of the server-side write path. * * Copyright (c) 2025, PostgreSQL Global Development Group * @@ -74,7 +74,7 @@ dwb_cycle_internal(int npages) rlocator.relNumber = 90000 + (i % 3); InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, (BlockNumber) i); - DWBAcquireSlot(&tag, &ref); + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); /* * A batch switch means the previous batch overflowed and was sealed @@ -313,7 +313,7 @@ test_dwb_leak(PG_FUNCTION_ARGS) rlocator.relNumber = 91000; InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, (BlockNumber) i); - DWBAcquireSlot(&tag, &ref); + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); if (do_publish) { memset(page, 'L', BLCKSZ); @@ -323,6 +323,137 @@ test_dwb_leak(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } +/* + * Occupy the whole ring without blocking: acquire and publish slots until + * no FREE batch remains and the open batch is full, keeping every ref (the + * refs die with the session). Sets up ring exhaustion for the + * backpressure tests. Meant for dwb_retire_workers = 0, where nothing + * seals or retires behind our back. Returns the number of slots taken. + */ +PG_FUNCTION_INFO_V1(test_dwb_fill_ring); +Datum +test_dwb_fill_ring(PG_FUNCTION_ARGS) +{ + int taken = 0; + static char page[BLCKSZ]; + + check_dwb_enabled(); + + for (;;) + { + int nfree = 0; + uint32 open_idx; + BufferTag tag; + DWBSlotRef ref; + RelFileLocator rlocator; + + CHECK_FOR_INTERRUPTS(); + + /* hard bound of the backend-local ref array */ + if (taken >= 2 * DWB_BATCH_MAX_PAGES - 1) + break; + + for (int i = 0; i < dwb_num_batches; i++) + if (DWBGetBatchState(i) == DWB_FREE) + nfree++; + open_idx = pg_atomic_read_u32(&DWBCtl->open_batch_idx[DWB_WCLASS_EVICTION]); + if (nfree == 0 && + (open_idx == DWB_INVALID_BATCH || + (pg_atomic_read_u32(&DWBCtl->batches[open_idx].next_slot_idx) & + (DWB_SEAL_BIT | DWB_IDX_MASK)) >= (uint32) dwb_batch_pages)) + break; /* one more acquire would block */ + + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = 1; + rlocator.relNumber = 95000; + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, (BlockNumber) taken); + + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); + memset(page, 'X', BLCKSZ); + DWBPublishImage(&ref, page, (XLogRecPtr) 0x6000000 + taken); + taken++; + } + + PG_RETURN_INT32(taken); +} + +/* + * Acquire (and optionally publish) npages slots WITH a ResourceOwner + * attachment, then raise an ERROR: the transaction abort must release the + * refs (poisoning unpublished slots), leaving the batch completable by a + * later seal. Exercises the abort path of the write path without a + * process exit. + */ +PG_FUNCTION_INFO_V1(test_dwb_abort_release); +Datum +test_dwb_abort_release(PG_FUNCTION_ARGS) +{ + int npages = PG_GETARG_INT32(0); + bool do_publish = PG_GETARG_BOOL(1); + static char page[BLCKSZ]; + + check_dwb_enabled(); + if (npages < 1 || npages >= dwb_batch_pages) + ereport(ERROR, (errmsg("npages out of range"))); + + for (int i = 0; i < npages; i++) + { + BufferTag tag; + DWBSlotRef ref; + RelFileLocator rlocator; + + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = 1; + rlocator.relNumber = 93000; + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, (BlockNumber) i); + + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, true, &ref); + if (do_publish) + { + memset(page, 'R', BLCKSZ); + DWBPublishImage(&ref, page, (XLogRecPtr) 0x4000000 + i); + } + } + + ereport(ERROR, (errmsg("test_dwb: deliberate abort with pending refs"))); + PG_RETURN_VOID(); /* unreachable */ +} + +/* + * Abort AFTER the batch is durable: acquire one slot with a ResourceOwner + * attachment, publish, seal, wait for DWB_FSYNCED, then ERROR. The abort + * cleanup takes the abandoned-slot repair path; the fake relation makes it + * exit through the dropped-relation branch, and the ref hand-off must + * still finish the batch (publication, RETIRING). + */ +PG_FUNCTION_INFO_V1(test_dwb_abort_after_fsync); +Datum +test_dwb_abort_after_fsync(PG_FUNCTION_ARGS) +{ + BufferTag tag; + DWBSlotRef ref; + RelFileLocator rlocator; + static char page[BLCKSZ]; + + check_dwb_enabled(); + + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = 1; + rlocator.relNumber = 94000; + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); + + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, true, &ref); + memset(page, 'F', BLCKSZ); + DWBPublishImage(&ref, page, (XLogRecPtr) 0x5000000); + + if (!DWBTrySealBatch(ref.batch_idx)) + ereport(ERROR, (errmsg("could not seal the batch under test"))); + DWBWaitBatchFsynced(&ref); + + ereport(ERROR, (errmsg("test_dwb: deliberate abort after batch fsync"))); + PG_RETURN_VOID(); /* unreachable */ +} + PG_FUNCTION_INFO_V1(test_dwb_force_seal); Datum test_dwb_force_seal(PG_FUNCTION_ARGS) @@ -378,7 +509,7 @@ test_dwb_open_stale(PG_FUNCTION_ARGS) rlocator.dbOid = 1; rlocator.relNumber = 92000; InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); - DWBAcquireSlot(&tag, &ref); + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); memset(page, 'S', BLCKSZ); DWBPublishImage(&ref, page, (XLogRecPtr) 0x3000000); diff --git a/src/test/modules/test_dwb/test_dwb.conf b/src/test/modules/test_dwb/test_dwb.conf index ff20a0a481f18..9a5fc08765b10 100644 --- a/src/test/modules/test_dwb/test_dwb.conf +++ b/src/test/modules/test_dwb/test_dwb.conf @@ -1,3 +1,7 @@ io_torn_pages_protection = double_writes dwb_num_batches = 16 dwb_batch_pages = 16 +# keep sealing and retirement under the test's control +dwb_retire_workers = 0 +bgwriter_lru_maxpages = 0 +autovacuum = off diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out index 37df27351d8af..300c8a4997e95 100644 --- a/src/test/regress/expected/stats.out +++ b/src/test/regress/expected/stats.out @@ -16,17 +16,20 @@ SHOW track_counts; -- must be on SELECT backend_type, object, context FROM pg_stat_io ORDER BY backend_type COLLATE "C", object COLLATE "C", context COLLATE "C"; backend_type|object|context +autovacuum launcher|dwb|normal autovacuum launcher|relation|bulkread autovacuum launcher|relation|init autovacuum launcher|relation|normal autovacuum launcher|wal|init autovacuum launcher|wal|normal +autovacuum worker|dwb|normal autovacuum worker|relation|bulkread autovacuum worker|relation|init autovacuum worker|relation|normal autovacuum worker|relation|vacuum autovacuum worker|wal|init autovacuum worker|wal|normal +background worker|dwb|normal background worker|relation|bulkread background worker|relation|bulkwrite background worker|relation|init @@ -35,14 +38,17 @@ background worker|relation|vacuum background worker|temp relation|normal background worker|wal|init background worker|wal|normal +background writer|dwb|normal background writer|relation|init background writer|relation|normal background writer|wal|init background writer|wal|normal +checkpointer|dwb|normal checkpointer|relation|init checkpointer|relation|normal checkpointer|wal|init checkpointer|wal|normal +client backend|dwb|normal client backend|relation|bulkread client backend|relation|bulkwrite client backend|relation|init @@ -51,6 +57,7 @@ client backend|relation|vacuum client backend|temp relation|normal client backend|wal|init client backend|wal|normal +io worker|dwb|normal io worker|relation|bulkread io worker|relation|bulkwrite io worker|relation|init @@ -59,6 +66,7 @@ io worker|relation|vacuum io worker|temp relation|normal io worker|wal|init io worker|wal|normal +slotsync worker|dwb|normal slotsync worker|relation|bulkread slotsync worker|relation|bulkwrite slotsync worker|relation|init @@ -67,6 +75,7 @@ slotsync worker|relation|vacuum slotsync worker|temp relation|normal slotsync worker|wal|init slotsync worker|wal|normal +standalone backend|dwb|normal standalone backend|relation|bulkread standalone backend|relation|bulkwrite standalone backend|relation|init @@ -74,6 +83,7 @@ standalone backend|relation|normal standalone backend|relation|vacuum standalone backend|wal|init standalone backend|wal|normal +startup|dwb|normal startup|relation|bulkread startup|relation|bulkwrite startup|relation|init @@ -83,6 +93,7 @@ startup|wal|init startup|wal|normal walreceiver|wal|init walreceiver|wal|normal +walsender|dwb|normal walsender|relation|bulkread walsender|relation|bulkwrite walsender|relation|init @@ -95,7 +106,7 @@ walsummarizer|wal|init walsummarizer|wal|normal walwriter|wal|init walwriter|wal|normal -(79 rows) +(90 rows) \a -- ensure that both seqscan and indexscan plans are allowed SET enable_seqscan TO on; From 9fe2303160181947fcf3fff4c83ee970611bafcd Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 24 Jul 2026 17:00:30 +0300 Subject: [PATCH 06/52] Fix fsync error-path accounting and close review gaps (Stage 2 follow-up) A review round over the Stage 2 commit surfaced one real corruption path and two accounting leaks, all rooted in the assumption that a retirement fsync failure is always a PANIC. That only holds with the default data_sync_retry = off; with data_sync_retry = on the process survives the error and the bookkeeping used to rot: * DWBSegmentFsyncBegin now unconditionally drops a leftover snapshot before deciding whether to take a new one. Previously, after an fsync ERROR between Begin and End, the checkpointer kept the stale snapshot armed; the End of the next successful fsync of an unrelated non-MD tag (CLOG/SLRU share pendingOps) consumed it, decrementing the failed segment's back-references and freeing its batches without their data being durable -- silent corruption after a crash. * DWBRetireSyncSegment no longer throws on a soft failure: it returns false (WARNING), the segment's bits stay for a later retry, and the caller's fsync_in_progress claim is always released. The OOM retire path escalates to PANIC instead: nothing ever revisits a DWB_OOM_RETIRING batch, and the path can run inside a ResourceOwner release callback, which must not fail. The proc-exit backstop now performs the abandoned-slot repair only for refs that were attached to a ResourceOwner. An owned ref alive at proc-exit implies the same-owner buffer-IO resource is alive too (it releases after the DWB ref within one owner, and DWBFinishPageWrite precedes TerminateBufferIO on the success path), so BM_IO_IN_PROGRESS is still ours and the repair is race-free; ownerless test refs never had the interlock, so writing would race concurrent flushes. Hardening: DWB_NUM_BATCHES_MAX ties the GUC maximum to the retire-side snapshot array; static asserts pin the no-padding requirement of the DWSegRef hash key and the semantic ordering of DWBatchState; DWBAcquireSlot memsets the seg ref it publishes; DWBPublishBatchSegSet asserts a non-empty seg_set. Comment fixes: the DWBTrySealBatch header no longer claims every seal funnels through it (the overflow writer calls DWBSealBatch directly), and the FlushBuffer comment no longer overstates PageSetChecksumCopy for all-zero new pages. New test coverage: * test_dwb_torn_repair stages a real table's block, tears it on disk and aborts: the ResourceOwner release must repair the block from the batch copy, verified by reading it back after a restart (001). * 004_retire_paths.pl proves a CHECKPOINT alone retires a parked batch through the DWBSegmentFsyncBegin/End wrap (fsync = on is required: with fsync = off ProcessSyncRequests skips the whole per-file block), and overflows DWSegmentHash to exercise the synchronous OOM retire. * 002 gains a dwb_retire_workers = 0 phase over a real eviction workload and an unlogged-bypass check that reads the ring content itself (test_dwb_ring_rel_slots) -- a normal CHECKPOINT skips unlogged buffers, so the shutdown checkpoint provides the flush. * 003 walks a victim through Stage A into Stage B on the real stall clocks, with no injection point. * 001 covers the DWB_EVICT_RESERVE boundary: a background-class fill stops with the reserve left free, an eviction-class fill drains it. * meson.build now runs all four TAP files (002/003 were missing). --- src/backend/storage/buffer/bufmgr.c | 7 +- src/backend/storage/dwb/dwb.c | 65 +++-- src/backend/storage/dwb/dwb_retire.c | 74 +++++- src/backend/utils/misc/guc_tables.c | 2 +- src/include/storage/dwb.h | 32 ++- src/test/modules/test_dwb/meson.build | 8 +- src/test/modules/test_dwb/t/001_dwb.pl | 53 +++- .../modules/test_dwb/t/002_flushbuffer.pl | 55 ++++ .../modules/test_dwb/t/003_backpressure.pl | 35 +++ .../modules/test_dwb/t/004_retire_paths.pl | 91 +++++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 20 +- src/test/modules/test_dwb/test_dwb.c | 250 ++++++++++++++++-- 12 files changed, 628 insertions(+), 64 deletions(-) create mode 100644 src/test/modules/test_dwb/t/004_retire_paths.pl diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 4b349ca9ee275..fd38ab4f74428 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -4379,8 +4379,11 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, * from there (full_page_writes replacement, see storage/dwb.h). Only * BM_PERMANENT buffers need this: unlogged relations are reset from * their init fork after a crash, so their torn writes don't matter. - * With data checksums required by the DWB, bufToWrite is always a - * private copy, stable regardless of concurrent hint-bit updates. + * Data checksums are required by the DWB, so for any page with content + * bufToWrite is a private copy, stable regardless of concurrent + * hint-bit updates; PageSetChecksumCopy returns the shared page only + * when it is all-zero new, where there are no tuples for hint bits to + * touch. */ if (DWBIsEnabled() && (buf_state & BM_PERMANENT) && !IsBootstrapProcessingMode()) diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index e4b0bd581be60..757e07f619faa 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -600,10 +600,15 @@ DWBLeaderWriteBatch(int batch_idx) */ /* - * Try to seal a batch if it is still an open, non-empty ALLOCATED one. - * Every seal initiator goes through here: overflow writers, retire workers - * acting on dwb_batch_timeout_ms, and writers whose DWBWaitBatchFsynced - * timed out on a batch nobody else sealed. + * Try to seal a batch if it is still an open, non-empty ALLOCATED one; the + * guards make it safe to call speculatively against any state. Entry point + * of the decentralized seal triggers: the lone-writer fast seal and the + * timeout seal in DWBWaitBatchFsynced, the no-worker-pool seal in + * DWBStagePageWrite, DWBForceSealOpenBatch, and the retire workers' + * force-seal on dwb_batch_timeout_ms. The overflow writer in + * DWBAcquireSlot calls DWBSealBatch directly instead: it has just consumed + * the first slot index past the cap, so it already knows the batch is full + * and non-empty. */ bool DWBTrySealBatch(int batch_idx) @@ -701,6 +706,8 @@ DWBAcquireSlot(const BufferTag *tag, int wclass, bool use_resowner, DWSegRef seg; bool found = false; + /* becomes a HASH_BLOBS key at publication: no padding garbage */ + memset(&seg, 0, sizeof(seg)); seg.rlocator = BufTagGetRelFileLocator(tag); seg.forknum = BufTagGetForkNum(tag); seg.segno = tag->blockNum / RELSEG_SIZE; @@ -951,13 +958,16 @@ DWBFinishPageWrite(const DWBSlotRef *ref) * (the abort path never clears BM_DIRTY), so newer content still reaches * the disk through a later flush. * - * This runs from the ResourceOwner release, BEFORE the buffer-IO cleanup: - * BM_IO_IN_PROGRESS of the failed flush is still ours, so no concurrent - * flush of the same page can be in flight and writing the (possibly stale) - * batch copy cannot overwrite a newer image. For the same reason the - * relation cannot be dropped or truncated under us — both invalidate the - * buffer first and that waits for our IO flag — so the ENOENT/short-file - * exits are pure defense (and serve test refs pointing at fake relations). + * This runs for refs that were attached to a ResourceOwner, either from the + * owner's release (BEFORE the buffer-IO cleanup) or from the proc-exit + * backstop when abort cleanup was cut short (see DWBProcExit): in both + * cases BM_IO_IN_PROGRESS of the failed flush is still ours, so no + * concurrent flush of the same page can be in flight and writing the + * (possibly stale) batch copy cannot overwrite a newer image. For the same + * reason the relation cannot be dropped or truncated under us — both + * invalidate the buffer first and that waits for our IO flag — so the + * ENOENT/short-file exits are pure defense (and serve test refs pointing at + * fake relations). * * Durability: our segment is in the batch's seg_set, and the seg_set is * published only after every ref (ours included) is gone, so retirement @@ -1044,6 +1054,7 @@ DWBAbandonRef(DWBPendingRef *pref) uint64 bit = UINT64CONST(1) << (ref.slot_idx % 64); pg_atomic_uint64 *word = &batch->slots_written_bitmap[ref.slot_idx / 64]; + bool had_owner = (pref->owner != NULL); pref->owner = NULL; pref->in_use = false; @@ -1063,12 +1074,19 @@ DWBAbandonRef(DWBPendingRef *pref) pg_atomic_fetch_or_u64(word, bit); ConditionVariableBroadcast(&batch->cv_state); } - else if (pg_atomic_read_u32(&batch->state) >= DWB_FSYNCED) + else if (had_owner && + pg_atomic_read_u32(&batch->state) >= DWB_FSYNCED) { /* * Copy published and the batch is durable, which means the writer * was at or past step 6: its smgrwrite may have failed halfway. * Make the data page whole again from the batch copy. + * + * Only for refs that were attached to a ResourceOwner: those are + * real write-path refs, and their BM_IO_IN_PROGRESS is still held + * here (on the proc-exit path too, see DWBProcExit). An ownerless + * (test) ref never had the buffer-IO interlock, so the repair write + * would race a concurrent flush of the same page. */ DWBRewriteAbandonedSlot(&ref); } @@ -1095,12 +1113,23 @@ ResOwnerReleaseDWBRef(Datum res) } /* - * Process-exit backstop for refs that no ResourceOwner released (test refs - * acquired without an owner; anything a nonstandard exit path missed). By - * this time LWLockReleaseAll has already dropped any content locks (ipc.c) - * and buffer-IO flags may be gone too, so unlike the ResourceOwner path - * the abandoned-slot rewrite here is best effort against concurrent - * flushes; the primary cleanup is the ResourceOwner one. + * Process-exit backstop for refs that no ResourceOwner released. + * + * An owned ref can only get here when abort cleanup was cut short before + * the ResourceOwner release phase (e.g. a FATAL thrown out of the abort + * path itself). In that case the buffer-IO resource of the failed flush + * was not released either: it lives in the SAME owner and releases AFTER + * the DWB ref (ascending priority within the phase, RELEASE_PRIO_BUFFER_IOS + * - 10 before RELEASE_PRIO_BUFFER_IOS; on the success path + * DWBFinishPageWrite likewise precedes TerminateBufferIO). So whenever an + * owned ref is still alive, BM_IO_IN_PROGRESS is still ours and the + * abandoned-slot repair is exactly as race-free as on the ResourceOwner + * path. It is also the last chance to repair: a FATAL exit does not + * trigger crash recovery, so no apply-pass would ever fix a torn page. + * + * Ownerless refs are test refs (DWBAcquireSlot with use_resowner = false); + * they never had the interlock and DWBAbandonRef skips the repair write for + * them. */ static void DWBProcExit(int code, Datum arg) diff --git a/src/backend/storage/dwb/dwb_retire.c b/src/backend/storage/dwb/dwb_retire.c index 6f4ac163a6b12..40160c0eac1d1 100644 --- a/src/backend/storage/dwb/dwb_retire.c +++ b/src/backend/storage/dwb/dwb_retire.c @@ -70,7 +70,7 @@ typedef struct DWBSegSyncSnap { int batch_idx; uint64 batch_id; - } pairs[1024]; /* dwb_num_batches max (GUC) */ + } pairs[DWB_NUM_BATCHES_MAX]; } DWBSegSyncSnap; static DWBSegSyncSnap seg_sync_snap; @@ -119,11 +119,19 @@ DWBNoteBatchFreed(void) /* * Fsync one segment for retirement purposes, tolerating a concurrently * dropped relation: the data-file writes of a dropped segment are moot, so - * ENOENT counts as covered. Any other failure follows the vanilla - * data_sync_retry policy (PANIC by default). + * ENOENT counts as covered. Returns true if the segment is covered (fsynced + * or dropped). + * + * A real fsync failure follows the vanilla data_sync_retry policy: PANIC by + * default, but with data_sync_retry = on the kernel is trusted to keep the + * dirty pages, so this must NOT throw -- the callers hold accounting state + * (the advisory fsync claim, the OOM batch state) that a longjmp would leak + * forever. Instead it WARNs and returns false; the segment's back-reference + * bits stay set and a later fsyncer retries. force_panic is for the one + * caller that has no later fsyncer to fall back on (DWBRetireBatchSyncOOM). */ -static void -DWBRetireSyncSegment(const DWSegRef *seg) +static bool +DWBRetireSyncSegment(const DWSegRef *seg, bool force_panic) { FileTag tag = DWBFileTagFromSegRef(seg); char path[MAXPGPATH]; @@ -134,12 +142,14 @@ DWBRetireSyncSegment(const DWSegRef *seg) { elog(DEBUG1, "DWB: segment \"%s\" dropped during retire, skipping fsync", path); - return; + return true; } - ereport(data_sync_elevel(ERROR), + ereport(force_panic ? PANIC : data_sync_elevel(WARNING), (errcode_for_file_access(), errmsg("could not fsync file \"%s\": %m", path))); + return false; } + return true; } /* @@ -148,6 +158,12 @@ DWBRetireSyncSegment(const DWSegRef *seg) * the batch, so an undersized hash degrades throughput instead of wedging * the ring. Runs with no locks held; the DWB_OOM_RETIRING state keeps * everyone else away from the batch. + * + * An fsync failure here is a PANIC even under data_sync_retry = on: nothing + * ever revisits a DWB_OOM_RETIRING batch (retire sweeps only collect + * DWB_RETIRING, and the hash bits were rolled back), so a soft failure would + * leak the batch until restart -- and this can run inside a ResourceOwner + * release callback, which must not fail (resowner.h). */ static void DWBRetireBatchSyncOOM(DWBatchCtl *batch) @@ -155,7 +171,7 @@ DWBRetireBatchSyncOOM(DWBatchCtl *batch) uint32 expected; for (uint32 i = 0; i < batch->n_segs; i++) - DWBRetireSyncSegment(&batch->seg_set[i]); + (void) DWBRetireSyncSegment(&batch->seg_set[i], true); expected = DWB_OOM_RETIRING; if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, DWB_FREE)) @@ -180,6 +196,14 @@ DWBPublishBatchSegSet(int batch_idx) bool oom = false; uint32 expected; + /* + * A sealed non-empty batch always has at least one segment (every slot + * reservation dedup-inserts its segment). Publishing an empty seg_set + * would move the batch to DWB_RETIRING with nothing to ever decrement it + * to FREE. + */ + Assert(batch->n_segs > 0); + LWLockAcquire(&batch->publish_lock, LW_EXCLUSIVE); pg_atomic_write_u32(&batch->seg_pending_count, batch->n_segs); @@ -284,8 +308,9 @@ DWBSegSnapBegin(const DWSegRef *seg) { DWSegEntry *entry; - /* a leftover active snapshot means the previous fsync ERROR'ed out - * between Begin and End: its bits were never cleared, just drop it */ + /* overwriting a leftover snapshot (an fsync that errored out between + * Begin and End) is a correct drop: its bits were never cleared and a + * later fsyncer covers them; see also DWBSegmentFsyncBegin */ seg_sync_snap.active = true; seg_sync_snap.seg = *seg; seg_sync_snap.npairs = 0; @@ -304,6 +329,7 @@ DWBSegSnapBegin(const DWSegRef *seg) int idx = (int) (w * 64) + bit; word &= word - 1; + Assert(seg_sync_snap.npairs < (int) lengthof(seg_sync_snap.pairs)); seg_sync_snap.pairs[seg_sync_snap.npairs].batch_idx = idx; /* @@ -400,6 +426,18 @@ DWBSegmentFsyncBegin(const FileTag *ftag) { DWSegRef seg; + /* + * Drop any leftover snapshot BEFORE deciding whether to take a new one. + * If a previous fsync ERROR'ed out between Begin and End (possible in + * the checkpointer with data_sync_retry = on, which survives the ERROR + * and keeps this process-local state), the early return below would + * otherwise leave the stale snapshot armed, and the End of the next + * successful fsync of an unrelated non-MD tag would decrement the stale + * segment's back-references -- freeing batches whose data-file fsync + * never succeeded. + */ + seg_sync_snap.active = false; + if (!DWBIsEnabled() || ftag->handler != SYNC_HANDLER_MD) return; @@ -451,6 +489,7 @@ DWBRetireSegment(const DWSegRef *seg) { DWSegEntry *entry; bool claimed = false; + bool covered; uint32 zero = 0; int freed; @@ -464,9 +503,14 @@ DWBRetireSegment(const DWSegRef *seg) if (!claimed) return 0; + /* + * DWBRetireSyncSegment does not throw on a soft (data_sync_retry = on) + * fsync failure, so the claim reset below always runs; on covered = + * false the snapshot is discarded and the bits stay for a retry. + */ DWBSegSnapBegin(seg); - DWBRetireSyncSegment(seg); - freed = DWBSegSnapEnd(true); + covered = DWBRetireSyncSegment(seg, false); + freed = DWBSegSnapEnd(covered); /* * Release the claim. The entry may have been removed (and even @@ -486,8 +530,10 @@ DWBRetireSegment(const DWSegRef *seg) /* * One retire sweep over all RETIRING batches, oldest first. worker_id >= 0 * restricts the sweep to that worker's segment partition; -1 sweeps - * everything (self-help of a writer stuck on a full ring, and the second - * retire point in ProcessSyncRequests-less paths). Returns batches freed. + * everything: the self-help of a writer stuck on a full ring + * (DWBOpenNewBatch) and the synchronous retire in DWBFinishPageWrite when + * there is no worker pool (dwb_retire_workers = 0, single-user mode). + * Returns batches freed. */ int DWBRetireAllSync(void) diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index f48adacd236dc..60ae0c0a911d5 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -2202,7 +2202,7 @@ struct config_int ConfigureNamesInt[] = NULL }, &dwb_num_batches, - 64, 16, 1024, + 64, 16, DWB_NUM_BATCHES_MAX, NULL, NULL, NULL }, { diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index b8ce1e7942c89..51c6889c8e298 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -65,11 +65,14 @@ extern PGDLLIMPORT int dwb_on_stall; #define DWBIsEnabled() (io_torn_pages_protection == DWB_PROTECT_DOUBLE_WRITES) /* - * Compile-time capacity limits (GUC maxima). + * Compile-time capacity limits (GUC maxima). Statically sized arrays + * (per-batch shmem arrays, the retire-side segment snapshot) rely on these, + * so the GUC bounds in guc_tables.c must use them, never bare literals. */ #define DWB_BATCH_MAX_PAGES 256 #define DWB_BATCH_MAX_SEGS DWB_BATCH_MAX_PAGES #define DWB_BITMAP_WORDS (DWB_BATCH_MAX_PAGES / 64) +#define DWB_NUM_BATCHES_MAX 1024 /* staging pool: 2 writer classes + 2 in-flight leader writes */ #define DWB_STAGING_BUFFERS 4 /* writer classes (3.6) */ @@ -177,6 +180,15 @@ typedef enum DWBatchState * DWSegmentHash OOM (3.5) */ } DWBatchState; +StaticAssertDecl(DWB_FREE < DWB_ALLOCATED && + DWB_ALLOCATED < DWB_SEALED && + DWB_SEALED < DWB_WRITTEN && + DWB_WRITTEN < DWB_FSYNCED && + DWB_FSYNCED < DWB_DATA_WRITTEN && + DWB_DATA_WRITTEN < DWB_RETIRING && + DWB_RETIRING < DWB_OOM_RETIRING, + "DWBatchState numeric order is semantic (progress tests)"); + typedef struct DWSegRef { RelFileLocator rlocator; @@ -184,6 +196,14 @@ typedef struct DWSegRef uint32 segno; } DWSegRef; +/* + * DWSegRef is a HASH_BLOBS key: hashed and compared as raw bytes, so it must + * not contain padding (which field-wise construction would leave undefined). + */ +StaticAssertDecl(sizeof(DWSegRef) == + sizeof(RelFileLocator) + sizeof(ForkNumber) + sizeof(uint32), + "DWSegRef has padding; unsafe as a HASH_BLOBS key"); + /* * Segment -> batch back-reference (3.5): one shmem hash entry per segment * that at least one RETIRING batch still needs fsynced. The bitmap is @@ -249,7 +269,15 @@ typedef struct DWBatchCtl int staging_idx; /* staging buffer; held from ALLOCATED until * the leader finishes the image pwrite */ XLogRecPtr max_page_lsn; - uint64 batch_id; /* monotonic, for ordering */ + uint64 batch_id; /* monotonic incarnation id. Written only at + * reopen, under DWBRingOpenLock; read under a + * held ref (which pins the incarnation) or + * racily by the retire side. The ABA + * re-check in DWBSegSnapEnd reads it under + * publish_lock, but that lock does not + * serialize against the reopen write: safety + * comes from the idempotent bitmap re-check + * plus id monotonicity (3.5). */ TimestampTz open_time; /* FREE -> ALLOCATED instant; drives * force-SEAL via dwb_batch_timeout_ms */ } DWBatchCtl; diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index c3dd8779096ec..069f3be429561 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -33,8 +33,14 @@ tests += { 'runningcheck': false, }, 'tap': { + 'env': { + 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', + }, 'tests': [ - 't/001_dwb.pl' + 't/001_dwb.pl', + 't/002_flushbuffer.pl', + 't/003_backpressure.pl', + 't/004_retire_paths.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/001_dwb.pl b/src/test/modules/test_dwb/t/001_dwb.pl index 8448b74c8c3c2..ba7b33f47fd9a 100644 --- a/src/test/modules/test_dwb/t/001_dwb.pl +++ b/src/test/modules/test_dwb/t/001_dwb.pl @@ -159,8 +159,8 @@ sub flip_byte '1', 'batch of the aborted transaction retires'); # An ERROR after the batch is durable: the abort cleanup goes through the -# abandoned-slot repair (the fake relation exits via the dropped-relation -# branch) and must still hand the batch over to retirement. +# abandoned-slot ref hand-off (the fake relation exits via the +# dropped-relation branch) and must still hand the batch over to retirement. ($rc, $out, $err) = $node->psql('postgres', 'SELECT test_dwb_abort_after_fsync()'); isnt($rc, 0, 'deliberate abort after batch fsync reported'); @@ -170,6 +170,55 @@ sub flip_byte $node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=16/, 'ring idle after the abort scenarios'); +# --- torn data page repaired from the batch copy on abort ---------------- + +# A REAL relation this time: test_dwb_torn_repair stages the pristine +# on-disk image of block 0 into the DWB, tears the block on disk, and +# aborts. The ResourceOwner release must rewrite the block from the +# durable batch copy (DWBRewriteAbandonedSlot). The restart proves the +# repair reached the data file: the buffer cache is dropped, and with data +# checksums a block left torn would make the read below fail. +$node->safe_psql('postgres', q( + CREATE TABLE dwb_repair AS + SELECT g AS id, repeat('r', 64) AS pad FROM generate_series(1, 100) g; +)); +$node->safe_psql('postgres', 'CHECKPOINT'); +my $filenode = + $node->safe_psql('postgres', "SELECT pg_relation_filenode('dwb_repair')"); +($rc, $out, $err) = + $node->psql('postgres', "SELECT test_dwb_torn_repair($filenode, 0)"); +isnt($rc, 0, 'deliberate abort after tearing the data page reported'); +like($err, qr/deliberate abort after tearing/, 'the tear scenario ran'); +is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), + '1', 'batch of the torn-page scenario retires'); +$node->restart; +is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_repair'), + '100', 'torn block repaired from the batch copy (checksum-clean read)'); + +# --- background writers leave the eviction reserve ----------------------- + +# DWB_EVICT_RESERVE = Max(2, 16/8) = 2 on this geometry: a background-class +# writer must stop opening batches once only the reserve is left, while an +# eviction-class writer may take the ring down to zero. +$bg = $node->background_psql('postgres'); +my $bg_taken = $bg->query_safe('SELECT test_dwb_fill_ring(true)'); +cmp_ok($bg_taken, '>', 0, 'background class filled the ring'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/free=2 /, 'background class stops at DWB_EVICT_RESERVE free batches'); +my $ev_taken = $bg->query_safe('SELECT test_dwb_fill_ring(false)'); +cmp_ok($ev_taken, '>', 0, 'eviction class still opens batches'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/free=0 /, 'eviction class may take the ring to zero'); +$bg->quit; +$node->poll_query_until('postgres', + "SELECT CASE WHEN test_dwb_force_seal(false) IS NOT NULL THEN " + . "CASE WHEN test_dwb_force_seal(true) IS NOT NULL THEN " + . "CASE WHEN test_dwb_retire() >= 0 THEN " + . "test_dwb_states() LIKE 'free=16 %' END END END") + or die 'timed out waiting for the ring to drain after the reserve scenario'; + # --- stale open must not hijack a reopened index ------------------------ ($rc, $out, $err) = diff --git a/src/test/modules/test_dwb/t/002_flushbuffer.pl b/src/test/modules/test_dwb/t/002_flushbuffer.pl index ebac14fa4b712..da5207c452006 100644 --- a/src/test/modules/test_dwb/t/002_flushbuffer.pl +++ b/src/test/modules/test_dwb/t/002_flushbuffer.pl @@ -71,6 +71,9 @@ # --- crash recovery: data intact, generation bumped ---------------------- +# Until the Stage 4 apply-pass lands this is a WAL-replay smoke test: it +# asserts that the DWB write path corrupts nothing and replay still works, +# not that torn pages get repaired from the ring. $node->safe_psql('postgres', "UPDATE dwb_t SET filler = repeat('z', 300) WHERE id % 7 = 0"); $node->stop('immediate'); @@ -79,4 +82,56 @@ is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), '50000', 'data intact after crash recovery'); +# --- unlogged relations bypass the double write buffer ------------------- + +# A normal CHECKPOINT skips unlogged buffers entirely (BufferSync writes +# only BM_PERMANENT ones), so the flush that exercises the FlushBuffer gate +# is the SHUTDOWN checkpoint of a clean restart, which writes all dirty +# buffers. The assertion reads the ring itself: a broken gate would leave +# dwb_ul's tags in batch files, and no stray permanent-page flush can fake +# that. +$node->safe_psql('postgres', q( + CREATE UNLOGGED TABLE dwb_ul AS + SELECT g AS id, repeat('u', 300) AS filler + FROM generate_series(1, 1000) g; + UPDATE dwb_ul SET filler = repeat('w', 300); +)); +my $ul_filenode = + $node->safe_psql('postgres', "SELECT pg_relation_filenode('dwb_ul')"); +my $t_filenode = + $node->safe_psql('postgres', "SELECT pg_relation_filenode('dwb_t')"); +my $rel_pre = $node->safe_psql('postgres', + "SELECT COALESCE(sum(writes), 0) FROM pg_stat_io " + . "WHERE object = 'relation' AND backend_type = 'checkpointer'"); +$node->restart; +cmp_ok( + $node->safe_psql('postgres', + "SELECT COALESCE(sum(writes), 0) FROM pg_stat_io " + . "WHERE object = 'relation' AND backend_type = 'checkpointer'"), + '>', $rel_pre, 'shutdown checkpoint flushed the unlogged pages'); +is( $node->safe_psql('postgres', + "SELECT test_dwb_ring_rel_slots($ul_filenode)"), + '0', 'no unlogged page ever entered the ring'); +cmp_ok( + $node->safe_psql('postgres', + "SELECT test_dwb_ring_rel_slots($t_filenode)"), + '>', 0, 'permanent pages did enter the ring (control)'); + +# --- the write path is self-sufficient without the worker pool ----------- + +# dwb_retire_workers = 0: writers seal immediately and retire synchronously +# in DWBFinishPageWrite; a real eviction workload must keep circulating and +# the ring must drain to all-free without any worker. +$node->append_conf('postgresql.conf', 'dwb_retire_workers = 0'); +$node->restart; +$node->safe_psql('postgres', + "UPDATE dwb_t SET filler = repeat('n', 300) WHERE id % 5 = 0"); +$node->safe_psql('postgres', 'CHECKPOINT'); +is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), + '50000', 'workload survived the no-pool write path'); +$node->poll_query_until('postgres', + "SELECT test_dwb_states() LIKE 'free=16 %'") + or die 'timed out waiting for the ring to drain without a worker pool'; +pass('ring drained to all-free without a worker pool'); + done_testing(); diff --git a/src/test/modules/test_dwb/t/003_backpressure.pl b/src/test/modules/test_dwb/t/003_backpressure.pl index 24ebd5e55766b..46cc9bc9e0c57 100644 --- a/src/test/modules/test_dwb/t/003_backpressure.pl +++ b/src/test/modules/test_dwb/t/003_backpressure.pl @@ -123,4 +123,39 @@ is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_dirty'), '1000', 'data intact after crash recovery'); +# --- Stage A warning fires on the real clock ------------------------------ + +# No injection point this time: shrink the real thresholds and let a victim +# writer walk through Stage A (WARNING after dwb_slow_warn_ms) into Stage B +# (ERROR after dwb_write_timeout_ms, dwb_on_stall = error). The bgwriter +# pause of Stage A has no SQL-visible probe and stays untested here. +$node->append_conf( + 'postgresql.conf', qq( +dwb_slow_warn_ms = 100 +dwb_write_timeout_ms = 1000 +)); +$node->reload; + +$filler = $node->background_psql('postgres'); +$taken = $filler->query_safe('SELECT test_dwb_fill_ring()'); +cmp_ok($taken, '>', 0, 'ring exhausted for the slow-warn scenario'); + +($rc, $out, $err) = $node->psql('postgres', 'SELECT test_dwb_cycle(1)'); +isnt($rc, 0, 'victim writer errors out on the real stall clock'); +like( + $err, + qr/double write buffer has no free batch after/, + 'Stage A warning reached the writer'); +like( + $err, + qr/double write buffer retirement made no progress/, + 'Stage B error reached the writer'); + +$filler->quit; +$node->poll_query_until('postgres', + "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " + . "CASE WHEN test_dwb_retire() >= 0 THEN " + . "test_dwb_states() LIKE 'free=16 %' END END") + or die 'timed out waiting for the ring to drain after the slow-warn scenario'; + done_testing(); diff --git a/src/test/modules/test_dwb/t/004_retire_paths.pl b/src/test/modules/test_dwb/t/004_retire_paths.pl new file mode 100644 index 0000000000000..1c940c4245339 --- /dev/null +++ b/src/test/modules/test_dwb/t/004_retire_paths.pl @@ -0,0 +1,91 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Retirement fallback paths: the checkpointer's ProcessSyncRequests as the +# only retire point, and the synchronous OOM retire when DWSegmentHash +# overflows. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_retire'); +$node->init; +# The default 64x64 geometry lets RETIRING batches pin more distinct +# segments (64 x 64 = 4096) than the smallest segment hash can hold; no +# workers and no background flushers keep retirement fully under the test's +# control. fsync must be ON: with fsync = off ProcessSyncRequests skips +# its whole per-file block, including the DWBSegmentFsyncBegin/End wrap +# this test exists to exercise. +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 64 +dwb_batch_pages = 64 +dwb_max_segments = 1024 +dwb_retire_workers = 0 +bgwriter_lru_maxpages = 0 +checkpoint_timeout = 1h +autovacuum = off +fsync = on +)); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); + +# --- a CHECKPOINT alone retires a batch (ProcessSyncRequests path) ------- + +$node->safe_psql('postgres', q( + CREATE TABLE dwb_ckpt AS + SELECT g AS id, repeat('c', 64) AS pad FROM generate_series(1, 100) g; +)); +my $filenode = + $node->safe_psql('postgres', "SELECT pg_relation_filenode('dwb_ckpt')"); + +# Warmup: run the same statements once and CHECKPOINT, so every catalog +# hint bit they dirty is flushed now. The real run below then leaves no +# dirty buffer behind, the final CHECKPOINT has nothing to feed through the +# DWB write path (whose no-pool finish would retire our batch as a side +# effect), and only the DWBSegmentFsyncBegin/End wrap of +# ProcessSyncRequests can free the parked batch. +$node->safe_psql('postgres', "SELECT test_dwb_checkpoint_pending($filenode)"); +$node->safe_psql('postgres', 'SELECT test_dwb_states()'); +$node->safe_psql('postgres', 'CHECKPOINT'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/^free=64 /, 'warmup batch retired'); + +$node->safe_psql('postgres', "SELECT test_dwb_checkpoint_pending($filenode)"); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/retiring=1/, 'one batch parked in RETIRING with a pending sync request'); +$node->safe_psql('postgres', 'CHECKPOINT'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/^free=64 /, 'CHECKPOINT alone retired the parked batch'); + +# --- segment hash overflow degrades to synchronous retire ---------------- + +my $log_offset = -s $node->logfile; +my ($rc, $out, $err) = + $node->psql('postgres', 'SELECT test_dwb_fill_segments(30)'); +is($rc, 0, 'segment fill survived the hash overflow'); +is($out, 30 * 64, 'thirty batches of unique segments published'); +like( + $err, + qr/double write buffer segment hash is full/, + 'hash overflow warning reached the publisher'); +ok( $node->log_contains('double write buffer segment hash is full', + $log_offset), + 'hash overflow logged'); + +# The batches parked in RETIRING drain through the normal sweep (dropped +# fake segments count as covered); the OOM-retired ones are already free. +$node->poll_query_until('postgres', + "SELECT CASE WHEN test_dwb_retire() >= 0 THEN " + . "test_dwb_states() LIKE 'free=64 %' END") + or die 'timed out waiting for the ring to drain after the hash overflow'; +pass('ring drained after the hash overflow'); + +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index aefb1a5618592..b0c78ec56d27c 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -15,6 +15,10 @@ CREATE FUNCTION test_dwb_ring_slots(current_only bool) RETURNS int STRICT AS 'MODULE_PATHNAME' LANGUAGE C; +CREATE FUNCTION test_dwb_ring_rel_slots(relnumber oid) + RETURNS int STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + CREATE FUNCTION test_dwb_states() RETURNS text STRICT AS 'MODULE_PATHNAME' LANGUAGE C; @@ -23,7 +27,7 @@ CREATE FUNCTION test_dwb_leak(npages int, do_publish bool) RETURNS void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; -CREATE FUNCTION test_dwb_force_seal() +CREATE FUNCTION test_dwb_force_seal(background bool DEFAULT false) RETURNS bool STRICT AS 'MODULE_PATHNAME' LANGUAGE C; @@ -35,7 +39,7 @@ CREATE FUNCTION test_dwb_open_stale() RETURNS void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; -CREATE FUNCTION test_dwb_fill_ring() +CREATE FUNCTION test_dwb_fill_ring(background bool DEFAULT false) RETURNS int STRICT AS 'MODULE_PATHNAME' LANGUAGE C; @@ -46,3 +50,15 @@ CREATE FUNCTION test_dwb_abort_release(npages int, do_publish bool) CREATE FUNCTION test_dwb_abort_after_fsync() RETURNS void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_torn_repair(relnumber oid, blkno int) + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_checkpoint_pending(relnumber oid) + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_fill_segments(nbatches int) + RETURNS int STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index 0f64c303279b6..7a5804b9a4ac0 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -20,10 +20,13 @@ #include #include "catalog/pg_tablespace_d.h" +#include "common/relpath.h" #include "fmgr.h" #include "miscadmin.h" +#include "storage/bufpage.h" #include "storage/dwb.h" #include "storage/fd.h" +#include "storage/smgr.h" #include "utils/builtins.h" PG_MODULE_MAGIC; @@ -132,20 +135,19 @@ test_dwb_stress(PG_FUNCTION_ARGS) /* * Validate the on-disk ring the way the apply-pass will: read every batch * file, check header, then count slots passing meta_crc (+ generation if - * current_only) + flags + image_crc. + * current_only) + flags + image_crc. With have_filter, count only slots + * whose tag belongs to the given relation (any generation) — used to prove + * that pages of a relation never entered the ring. */ -PG_FUNCTION_INFO_V1(test_dwb_ring_slots); -Datum -test_dwb_ring_slots(PG_FUNCTION_ARGS) +static int +count_ring_slots(bool current_only, bool have_filter, Oid relnumber) { - bool current_only = PG_GETARG_BOOL(0); DWBControlFileData control; Size meta_region; DWSlotMeta *metas; char *image; int valid = 0; - check_dwb_enabled(); if (!DWBReadControlFile(&control, false)) pg_unreachable(); @@ -226,6 +228,9 @@ test_dwb_ring_slots(PG_FUNCTION_ARGS) continue; if (current_only && meta->generation != control.generation) continue; + if (have_filter && + BufTagGetRelNumber(&meta->tag) != (RelFileNumber) relnumber) + continue; errno = 0; r = pg_pread(fd, image, BLCKSZ, @@ -255,7 +260,28 @@ test_dwb_ring_slots(PG_FUNCTION_ARGS) pfree(metas); pfree(image); - PG_RETURN_INT32(valid); + return valid; +} + +PG_FUNCTION_INFO_V1(test_dwb_ring_slots); +Datum +test_dwb_ring_slots(PG_FUNCTION_ARGS) +{ + bool current_only = PG_GETARG_BOOL(0); + + check_dwb_enabled(); + PG_RETURN_INT32(count_ring_slots(current_only, false, InvalidOid)); +} + +/* slots of one relation, any generation: 0 = never entered the ring */ +PG_FUNCTION_INFO_V1(test_dwb_ring_rel_slots); +Datum +test_dwb_ring_rel_slots(PG_FUNCTION_ARGS) +{ + Oid relnumber = PG_GETARG_OID(0); + + check_dwb_enabled(); + PG_RETURN_INT32(count_ring_slots(false, true, relnumber)); } PG_FUNCTION_INFO_V1(test_dwb_states); @@ -287,7 +313,10 @@ test_dwb_states(PG_FUNCTION_ARGS) /* * Acquire (and optionally publish) npages slots and return WITHOUT * releasing them: the refs stay pending, so closing the session exercises - * DWBProcExit's poison (unpublished) or orphan (published) path. + * DWBProcExit. Unpublished slots get poisoned; published ownerless refs + * just drop their batch ref (the repair write is reserved for + * ResourceOwner-attached refs), leaving the batch completable by a later + * seal. */ PG_FUNCTION_INFO_V1(test_dwb_leak); Datum @@ -324,16 +353,22 @@ test_dwb_leak(PG_FUNCTION_ARGS) } /* - * Occupy the whole ring without blocking: acquire and publish slots until - * no FREE batch remains and the open batch is full, keeping every ref (the - * refs die with the session). Sets up ring exhaustion for the - * backpressure tests. Meant for dwb_retire_workers = 0, where nothing - * seals or retires behind our back. Returns the number of slots taken. + * Occupy the ring without blocking: acquire and publish slots until no + * openable FREE batch remains and the open batch is full, keeping every ref + * (the refs die with the session). Sets up ring exhaustion for the + * backpressure tests. With background = true the slots are taken in the + * BACKGROUND writer class, which must stop opening batches once only + * DWB_EVICT_RESERVE FREE ones are left. Meant for dwb_retire_workers = 0, + * where nothing seals or retires behind our back. Returns the number of + * slots taken. */ PG_FUNCTION_INFO_V1(test_dwb_fill_ring); Datum test_dwb_fill_ring(PG_FUNCTION_ARGS) { + bool background = PG_GETARG_BOOL(0); + int wclass = background ? DWB_WCLASS_BACKGROUND : DWB_WCLASS_EVICTION; + int reserve = background ? DWB_EVICT_RESERVE : 0; int taken = 0; static char page[BLCKSZ]; @@ -356,8 +391,8 @@ test_dwb_fill_ring(PG_FUNCTION_ARGS) for (int i = 0; i < dwb_num_batches; i++) if (DWBGetBatchState(i) == DWB_FREE) nfree++; - open_idx = pg_atomic_read_u32(&DWBCtl->open_batch_idx[DWB_WCLASS_EVICTION]); - if (nfree == 0 && + open_idx = pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]); + if (nfree <= reserve && (open_idx == DWB_INVALID_BATCH || (pg_atomic_read_u32(&DWBCtl->batches[open_idx].next_slot_idx) & (DWB_SEAL_BIT | DWB_IDX_MASK)) >= (uint32) dwb_batch_pages)) @@ -365,10 +400,10 @@ test_dwb_fill_ring(PG_FUNCTION_ARGS) rlocator.spcOid = DEFAULTTABLESPACE_OID; rlocator.dbOid = 1; - rlocator.relNumber = 95000; + rlocator.relNumber = 95000 + (background ? 1000 : 0); InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, (BlockNumber) taken); - DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); + DWBAcquireSlot(&tag, wclass, false, &ref); memset(page, 'X', BLCKSZ); DWBPublishImage(&ref, page, (XLogRecPtr) 0x6000000 + taken); taken++; @@ -421,10 +456,11 @@ test_dwb_abort_release(PG_FUNCTION_ARGS) /* * Abort AFTER the batch is durable: acquire one slot with a ResourceOwner - * attachment, publish, seal, wait for DWB_FSYNCED, then ERROR. The abort - * cleanup takes the abandoned-slot repair path; the fake relation makes it - * exit through the dropped-relation branch, and the ref hand-off must - * still finish the batch (publication, RETIRING). + * attachment, publish, seal, wait for DWB_FSYNCED, then ERROR. This + * exercises the REF HAND-OFF of the abort path: the fake relation makes the + * repair exit through the dropped-relation branch, and the last-ref drop + * must still finish the batch (publication, RETIRING). The repair write + * itself is exercised by test_dwb_torn_repair on a real relation. */ PG_FUNCTION_INFO_V1(test_dwb_abort_after_fsync); Datum @@ -454,12 +490,182 @@ test_dwb_abort_after_fsync(PG_FUNCTION_ARGS) PG_RETURN_VOID(); /* unreachable */ } +/* + * Torn-page repair end to end on a REAL relation: read the current on-disk + * image of one block, stage it into the DWB with a ResourceOwner-attached + * ref, make the batch durable, then deliberately tear the block on disk and + * abort. The ResourceOwner release must rewrite the block from the batch + * copy (DWBRewriteAbandonedSlot); the TAP test verifies the on-disk content + * after a restart, where a failed repair surfaces as a checksum error. + */ +PG_FUNCTION_INFO_V1(test_dwb_torn_repair); +Datum +test_dwb_torn_repair(PG_FUNCTION_ARGS) +{ + Oid relnumber = PG_GETARG_OID(0); + BlockNumber blkno = (BlockNumber) PG_GETARG_INT32(1); + BufferTag tag; + DWBSlotRef ref; + RelFileLocator rlocator; + RelPathStr relpath; + static PGAlignedBlock image; + static char junk[BLCKSZ / 2]; + int fd; + + check_dwb_enabled(); + + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = MyDatabaseId; + rlocator.relNumber = relnumber; + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, blkno); + + relpath = relpathperm(rlocator, MAIN_FORKNUM); + fd = OpenTransientFile(relpath.str, O_RDWR | PG_BINARY); + if (fd < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", relpath.str))); + errno = 0; + if (pg_pread(fd, image.data, BLCKSZ, (off_t) blkno * BLCKSZ) != BLCKSZ) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read block %u of file \"%s\": %m", + blkno, relpath.str))); + + /* stage the pristine image; the abort below must put it back */ + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, true, &ref); + DWBPublishImage(&ref, image.data, PageGetLSN((Page) image.data)); + if (!DWBTrySealBatch(ref.batch_idx)) + ereport(ERROR, (errmsg("could not seal the batch under test"))); + DWBWaitBatchFsynced(&ref); + + /* simulate a torn smgrwrite: clobber the second half of the block */ + memset(junk, 0x7F, sizeof(junk)); + errno = 0; + if (pg_pwrite(fd, junk, sizeof(junk), + (off_t) blkno * BLCKSZ + BLCKSZ / 2) != (ssize_t) sizeof(junk)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not tear block %u of file \"%s\": %m", + blkno, relpath.str))); + if (CloseTransientFile(fd) != 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", relpath.str))); + + ereport(ERROR, + (errmsg("test_dwb: deliberate abort after tearing the data page"))); + PG_RETURN_VOID(); /* unreachable */ +} + +/* + * Leave one batch RETIRING with a REAL segment in DWSegmentHash and a + * pending checkpointer sync request for that segment: stage one real block, + * make the batch durable, write the block through smgrwrite (which + * registers the sync request), and release the ref. With + * dwb_retire_workers = 0 and no explicit test_dwb_retire() call, only the + * checkpointer's ProcessSyncRequests -- wrapped by + * DWBSegmentFsyncBegin/End -- can retire the batch: the TAP test asserts + * that a CHECKPOINT alone frees the ring. + */ +PG_FUNCTION_INFO_V1(test_dwb_checkpoint_pending); +Datum +test_dwb_checkpoint_pending(PG_FUNCTION_ARGS) +{ + Oid relnumber = PG_GETARG_OID(0); + BufferTag tag; + DWBSlotRef ref; + RelFileLocator rlocator; + SMgrRelation reln; + static PGAlignedBlock image; + + check_dwb_enabled(); + + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = MyDatabaseId; + rlocator.relNumber = relnumber; + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); + + reln = smgropen(rlocator, INVALID_PROC_NUMBER); + smgrread(reln, MAIN_FORKNUM, 0, image.data); + + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); + DWBPublishImage(&ref, image.data, PageGetLSN((Page) image.data)); + if (!DWBTrySealBatch(ref.batch_idx)) + ereport(ERROR, (errmsg("could not seal the batch under test"))); + DWBWaitBatchFsynced(&ref); + + /* the data-file write; registers the checkpointer sync request */ + smgrwrite(reln, MAIN_FORKNUM, 0, image.data, false); + + DWBReleaseSlot(&ref); /* last ref: publication, RETIRING */ + PG_RETURN_VOID(); +} + +/* + * Publish nbatches full batches whose slots all point at DISTINCT fake + * segments, so that RETIRING batches accumulate DWSegmentHash entries until + * the hash overflows and publication degrades to the synchronous OOM retire + * (WARNING "segment hash is full", DWB_OOM_RETIRING, batch freed by the + * publisher). Meant for dwb_retire_workers = 0 so the RETIRING batches + * keep their entries pinned. Returns the number of slots published. + */ +PG_FUNCTION_INFO_V1(test_dwb_fill_segments); +Datum +test_dwb_fill_segments(PG_FUNCTION_ARGS) +{ + int nbatches = PG_GETARG_INT32(0); + int nsegs = 0; + static char page[BLCKSZ]; + static uint32 next_relnumber = 200000; + + check_dwb_enabled(); + if (nbatches < 1 || nbatches > dwb_num_batches) + ereport(ERROR, (errmsg("nbatches out of range"))); + + for (int b = 0; b < nbatches; b++) + { + DWBSlotRef refs[DWB_BATCH_MAX_PAGES]; + + CHECK_FOR_INTERRUPTS(); + + for (int i = 0; i < dwb_batch_pages; i++) + { + BufferTag tag; + RelFileLocator rlocator; + + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = 1; + rlocator.relNumber = next_relnumber++; + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); + + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &refs[i]); + /* the whole batch must be ours for the seal below to cover it */ + if (refs[i].batch_idx != refs[0].batch_idx) + ereport(ERROR, + (errmsg("segment-fill batch split unexpectedly"))); + memset(page, 'S', BLCKSZ); + DWBPublishImage(&refs[i], page, (XLogRecPtr) 0x7000000 + nsegs); + nsegs++; + } + if (!DWBTrySealBatch(refs[0].batch_idx)) + ereport(ERROR, (errmsg("could not seal a segment-fill batch"))); + DWBWaitBatchFsynced(&refs[0]); + for (int i = 0; i < dwb_batch_pages; i++) + DWBReleaseSlot(&refs[i]); + } + PG_RETURN_INT32(nsegs); +} + PG_FUNCTION_INFO_V1(test_dwb_force_seal); Datum test_dwb_force_seal(PG_FUNCTION_ARGS) { + bool background = PG_GETARG_BOOL(0); + check_dwb_enabled(); - PG_RETURN_BOOL(DWBForceSealOpenBatch(DWB_WCLASS_EVICTION)); + PG_RETURN_BOOL(DWBForceSealOpenBatch(background ? DWB_WCLASS_BACKGROUND + : DWB_WCLASS_EVICTION)); } PG_FUNCTION_INFO_V1(test_dwb_retire); From ad7acfcc5892974bf83f1b6f6645f974e8aa37dc Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 24 Jul 2026 20:04:42 +0300 Subject: [PATCH 07/52] Fix exit-callback ordering and error-proof the repair path (Stage 2 follow-up) A cross-agent review round over the Stage 2 commits surfaced three process-lifecycle and accounting defects, closed here together with the regression coverage it asked for. The exit backstop moves from on_proc_exit to before_shmem_exit. Dropping the last ref of a durable batch publishes its seg_set under publish_lock and DWBSegHashLock, which is only legal while the PGPROC is alive -- on_proc_exit callbacks run after ProcKill, and LWLockAcquire asserts a live PGPROC under a postmaster. Because the backstop may now run before the ResourceOwner release of the same refs, DWBAbandonRef became idempotent: whichever side runs second finds in_use = false and returns. test_dwb_leak_fsynced pins the scenario: a session exits holding the LAST ref of a DWB_FSYNCED batch, so the FSYNCED -> RETIRING hand-off executes inside the exit callback itself. The abandoned-slot repair path is now allocation-free. It runs from release callbacks that must not fail, but OpenTransientFile can ERROR in reserveAllocatedDesc and PathNameOpenFile in its VFD bookkeeping. DWBReadSlotImage opens the batch file itself with BasicOpenFile (no allocations; recovers from EMFILE by closing LRU VFDs) and reads with raw pg_pread under the DWB_BATCH_READ wait event; DWBRewriteAbandonedSlot likewise switches to BasicOpenFile. Every failure on the path, opens and closes included, is a PANIC. The retire worker pool now verifies that its workers actually fit. RegisterBackgroundWorker only LOGs "too many background workers", so a pool registered after the logical replication launcher could silently come up short. bgworker.c exposes GetNumRegisteredBackgroundWorkers() -- counting only successful registrations; the previous pre-increment also overcounted failed attempts -- and DWBRetireWorkersRegister FATALs when dwb_retire_workers exceeds the slots that remain free. A TAP test asserts the refusal at max_worker_processes = 1. pgstat_count_io_op_time() feeds pg_stat_database's blk_write_time / blk_read_time only for relation and temp-relation objects now: the old "anything but WAL" condition also counted DWB batch writes, doubling the apparent block write time of a page that goes through the ring. New data_sync_retry regression coverage in 004_retire_paths.pl: test_dwb_stale_snapshot replays a checkpointer fsync ERROR between DWBSegmentFsyncBegin/End followed by a successful non-MD sync -- the parked batch must stay RETIRING; and a directory planted at a fake segment's path makes the retire fsync fail with EISDIR under data_sync_retry = on -- the sweep must not throw, the batch must stay, and the segment must be coverable by the next sweep after the obstacle is removed, proving the advisory claim was released. All DWB typedefs are added to typedefs.list and the touched files are pgindent-clean. --- src/backend/postmaster/bgworker.c | 26 +++- src/backend/storage/buffer/bufmgr.c | 21 ++- src/backend/storage/dwb/dwb.c | 147 ++++++++++-------- src/backend/storage/dwb/dwb_file.c | 28 +++- src/backend/storage/dwb/dwb_recovery.c | 4 +- src/backend/storage/dwb/dwb_retire.c | 53 ++++--- src/backend/utils/activity/pgstat_io.c | 12 +- src/include/postmaster/bgworker.h | 3 + src/include/storage/dwb.h | 15 +- src/test/modules/test_dwb/t/001_dwb.pl | 38 +++++ .../modules/test_dwb/t/004_retire_paths.pl | 53 +++++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 12 ++ src/test/modules/test_dwb/test_dwb.c | 110 ++++++++++++- src/tools/pgindent/typedefs.list | 14 ++ 14 files changed, 423 insertions(+), 113 deletions(-) diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c index 8524dd0573296..aa13f85466260 100644 --- a/src/backend/postmaster/bgworker.c +++ b/src/backend/postmaster/bgworker.c @@ -940,11 +940,27 @@ BackgroundWorkerUnblockSignals(void) * function of a module library that's loaded by shared_preload_libraries; * otherwise it will have no effect. */ +/* static background workers registered so far (against max_worker_processes) */ +static int numworkers = 0; + +/* + * Report how many static background workers have been registered so far. + * + * RegisterBackgroundWorker only LOGs when the limit is exceeded, so an + * in-core pool registered late in startup (after the logical replication + * launcher) uses this to verify that its workers actually fit and to fail + * loudly otherwise. + */ +int +GetNumRegisteredBackgroundWorkers(void) +{ + return numworkers; +} + void RegisterBackgroundWorker(BackgroundWorker *worker) { RegisteredBgWorker *rw; - static int numworkers = 0; /* * Static background workers can only be registered in the postmaster @@ -1001,7 +1017,7 @@ RegisterBackgroundWorker(BackgroundWorker *worker) * towards the MAX_BACKENDS limit elsewhere. For now, it doesn't seem * important to relax this restriction. */ - if (++numworkers > max_worker_processes) + if (numworkers >= max_worker_processes) { ereport(LOG, (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), @@ -1034,6 +1050,12 @@ RegisterBackgroundWorker(BackgroundWorker *worker) rw->rw_terminate = false; dlist_push_head(&BackgroundWorkerList, &rw->rw_lnode); + + /* + * Count only successful registrations, so that + * GetNumRegisteredBackgroundWorkers() reflects the actual list. + */ + numworkers++; } /* diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index fd38ab4f74428..d3827f712fc0f 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -4375,15 +4375,14 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, /* * Double write buffer path: before the data-file write, make the copy - * durable in pg_dwb/ so that a torn smgrwrite can always be repaired - * from there (full_page_writes replacement, see storage/dwb.h). Only - * BM_PERMANENT buffers need this: unlogged relations are reset from - * their init fork after a crash, so their torn writes don't matter. - * Data checksums are required by the DWB, so for any page with content - * bufToWrite is a private copy, stable regardless of concurrent - * hint-bit updates; PageSetChecksumCopy returns the shared page only - * when it is all-zero new, where there are no tuples for hint bits to - * touch. + * durable in pg_dwb/ so that a torn smgrwrite can always be repaired from + * there (full_page_writes replacement, see storage/dwb.h). Only + * BM_PERMANENT buffers need this: unlogged relations are reset from their + * init fork after a crash, so their torn writes don't matter. Data + * checksums are required by the DWB, so for any page with content + * bufToWrite is a private copy, stable regardless of concurrent hint-bit + * updates; PageSetChecksumCopy returns the shared page only when it is + * all-zero new, where there are no tuples for hint bits to touch. */ if (DWBIsEnabled() && (buf_state & BM_PERMANENT) && !IsBootstrapProcessingMode()) @@ -4427,8 +4426,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, { /* * Step 6b: start kernel writeback of the page now so the segment - * fsync that retires the batch becomes a cheap barrier instead of - * a full flush. Not durability — that comes from the fsync. + * fsync that retires the batch becomes a cheap barrier instead of a + * full flush. Not durability — that comes from the fsync. */ if (dwb_writeback) smgrwriteback(reln, BufTagGetForkNum(&buf->tag), diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 757e07f619faa..1b80163f537cf 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -235,8 +235,8 @@ DWBStagingAlloc(void) /* * A buffer frees once its leader finishes the image pwrite; - * retirement broadcasts cv_free_batch too, so just re-check on - * every wake-up. The timeout only paces the stall clock. + * retirement broadcasts cv_free_batch too, so just re-check on every + * wake-up. The timeout only paces the stall clock. */ (void) ConditionVariableTimedSleep(&DWBCtl->cv_free_batch, 1000, WAIT_EVENT_DWB_FREE_BATCH); @@ -288,23 +288,23 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) /* * Reserve the staging buffer before taking the lock: the wait for a * free buffer can be long, and no sleeping (or interruptible) point - * may exist below, where we hold DWBRingOpenLock with a batch - * already taken out of DWB_FREE. + * may exist below, where we hold DWBRingOpenLock with a batch already + * taken out of DWB_FREE. */ staging_idx = DWBStagingAlloc(); LWLockAcquire(DWBRingOpenLock, LW_EXCLUSIVE); /* - * Someone else already replaced the open batch: done. Comparing - * the index alone is not enough: the ring reuses indexes, so by the - * time a slow opener gets here, old_idx may name a NEW live - * incarnation of the same slot (sealed, retired, freed and reopened - * behind our back), and replacing it would orphan that live batch - * together with its staging buffer. SEAL_BIT disambiguates the - * incarnations: it is set from SEAL through FREE and cleared only - * by the re-initialization below, under this same lock — so the - * open batch needs replacing if and only if its SEAL_BIT is set. + * Someone else already replaced the open batch: done. Comparing the + * index alone is not enough: the ring reuses indexes, so by the time + * a slow opener gets here, old_idx may name a NEW live incarnation of + * the same slot (sealed, retired, freed and reopened behind our + * back), and replacing it would orphan that live batch together with + * its staging buffer. SEAL_BIT disambiguates the incarnations: it is + * set from SEAL through FREE and cleared only by the + * re-initialization below, under this same lock — so the open batch + * needs replacing if and only if its SEAL_BIT is set. */ { uint32 cur = pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]); @@ -326,8 +326,8 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) * DWB_EVICT_RESERVE of them for user evictions, so that a * checkpoint's BufferSync storm cannot eat the ring from under * latency-critical paths. FREE->ALLOCATED happens only under - * DWBRingOpenLock, and concurrent retirements only grow the count, - * so the check cannot overestimate. + * DWBRingOpenLock, and concurrent retirements only grow the count, so + * the check cannot overestimate. */ for (int i = 0; i < dwb_num_batches; i++) if (pg_atomic_read_u32(&DWBCtl->batches[i].state) == DWB_FREE) @@ -385,11 +385,11 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) /* * No usable FREE batch. Help ourselves before waiting: sweep the - * RETIRING batches synchronously. Under normal operation the - * worker pool keeps the ring ahead of the writers and this path is - * rare; when it does run, the per-segment claim keeps us and the - * workers from duplicating fsyncs. This is also what keeps the - * ring alive with dwb_retire_workers = 0 and in single-user mode. + * RETIRING batches synchronously. Under normal operation the worker + * pool keeps the ring ahead of the writers and this path is rare; + * when it does run, the per-segment claim keeps us and the workers + * from duplicating fsyncs. This is also what keeps the ring alive + * with dwb_retire_workers = 0 and in single-user mode. */ if (DWBRetireAllSync() > 0) continue; @@ -419,11 +419,11 @@ DWBSealBatch(int batch_idx) uint32 expected; /* - * Get everything the critical section below could fail at out of the - * way while failure is still harmless (the seal has not been attempted - * yet, so on ERROR the batch stays ALLOCATED and any other writer can - * seal it later): the one-time leader allocations, the batch file VFD, - * and this backend's condition-variable wait event set (the first + * Get everything the critical section below could fail at out of the way + * while failure is still harmless (the seal has not been attempted yet, + * so on ERROR the batch stays ALLOCATED and any other writer can seal it + * later): the one-time leader allocations, the batch file VFD, and this + * backend's condition-variable wait event set (the first * ConditionVariablePrepareToSleep of a backend allocates it). */ if (leader_metas == NULL) @@ -476,10 +476,10 @@ DWBSealBatch(int batch_idx) /* * Pin the batch with a leader ref for the duration of the write. All - * writers may exit while we write (dropping their refs), and the - * FSYNCED -> RETIRING hand-off runs when the last ref drops: the pin - * guarantees ref_count stays above zero until DWB_FSYNCED is reached, - * so the hand-off always has exactly one well-defined owner. + * writers may exit while we write (dropping their refs), and the FSYNCED + * -> RETIRING hand-off runs when the last ref drops: the pin guarantees + * ref_count stays above zero until DWB_FSYNCED is reached, so the + * hand-off always has exactly one well-defined owner. */ pg_atomic_fetch_add_u32(&batch->ref_count, 1); @@ -515,9 +515,9 @@ DWBLeaderWriteBatch(int batch_idx) /* * Coverage wait is memcpy-bound: writers do no I/O between reserving a - * slot and setting their bit. The timeout is a defensive backstop - * (e.g. a writer stopped in a debugger); dead writers are covered by - * ref cleanup marking their slots DWB_SLOT_ABORTED. + * slot and setting their bit. The timeout is a defensive backstop (e.g. + * a writer stopped in a debugger); dead writers are covered by ref + * cleanup marking their slots DWB_SLOT_ABORTED. */ ConditionVariablePrepareToSleep(&batch->cv_state); for (;;) @@ -662,7 +662,13 @@ DWBAcquireSlot(const BufferTag *tag, int wclass, bool use_resowner, if (!cleanup_registered) { - on_proc_exit(DWBProcExit, 0); + /* + * before_shmem_exit, NOT on_proc_exit: dropping the last ref of a + * durable batch publishes its seg_set under LWLocks, which is only + * legal while our PGPROC is alive — on_proc_exit callbacks run + * after ProcKill has released it. + */ + before_shmem_exit(DWBProcExit, 0); cleanup_registered = true; } @@ -791,13 +797,12 @@ DWBWaitBatchFsynced(const DWBSlotRef *ref) /* * A lone writer has nobody to batch with: sequential flush streams - * (recovery, a backend evicting page after page, BufferSync) reach - * this wait one page at a time, and paying dwb_batch_timeout_ms per - * page would dominate the stream. If our ref is the only one on a - * still-open batch, seal right away; under concurrency ref_count > 1 - * keeps the rendezvous window open for the timeout. A racing second - * writer merely bounces to the next batch — sealing is valid at any - * moment. + * (recovery, a backend evicting page after page, BufferSync) reach this + * wait one page at a time, and paying dwb_batch_timeout_ms per page would + * dominate the stream. If our ref is the only one on a still-open batch, + * seal right away; under concurrency ref_count > 1 keeps the rendezvous + * window open for the timeout. A racing second writer merely bounces to + * the next batch — sealing is valid at any moment. */ if (pg_atomic_read_u32(&batch->ref_count) == 1) (void) DWBTrySealBatch(ref->batch_idx); @@ -917,9 +922,9 @@ DWBStagePageWrite(const BufferTag *tag, const char *image, DWBPublishImage(ref, image, page_lsn); /* - * With no worker pool (dwb_retire_workers = 0, single-user mode) a - * lonely batch would only seal via the wait timeout below; seal it - * right away instead of paying dwb_batch_timeout_ms per page. + * With no worker pool (dwb_retire_workers = 0, single-user mode) a lonely + * batch would only seal via the wait timeout below; seal it right away + * instead of paying dwb_batch_timeout_ms per page. */ if (dwb_retire_workers == 0 || !IsUnderPostmaster) (void) DWBTrySealBatch(ref->batch_idx); @@ -972,6 +977,12 @@ DWBFinishPageWrite(const DWBSlotRef *ref) * Durability: our segment is in the batch's seg_set, and the seg_set is * published only after every ref (ours included) is gone, so retirement * fsyncs this segment strictly after this write. + * + * Runs from release callbacks that must not fail, so the whole path is + * allocation-free: BasicOpenFile + raw pg_pwrite here (and the same inside + * DWBReadSlotImage) instead of OpenTransientFile/VFD, whose descriptor + * reservation and name bookkeeping can throw ERROR. Every failure other + * than the dropped/truncated-relation exits is PANIC. */ static void DWBRewriteAbandonedSlot(const DWBSlotRef *ref) @@ -996,7 +1007,7 @@ DWBRewriteAbandonedSlot(const DWBSlotRef *ref) else snprintf(path, MAXPGPATH, "%s.%u", relpath.str, segno); - fd = OpenTransientFile(path, O_RDWR | PG_BINARY); + fd = BasicOpenFile(path, O_RDWR | PG_BINARY); if (fd < 0) { if (errno == ENOENT) @@ -1015,7 +1026,7 @@ DWBRewriteAbandonedSlot(const DWBSlotRef *ref) if (off + BLCKSZ > st.st_size) { /* segment truncated: the write is moot */ - CloseTransientFile(fd); + close(fd); return; } @@ -1032,7 +1043,7 @@ DWBRewriteAbandonedSlot(const DWBSlotRef *ref) tag.blockNum, path))); } - if (CloseTransientFile(fd) != 0) + if (close(fd) != 0) ereport(PANIC, (errcode_for_file_access(), errmsg("could not close file \"%s\": %m", path))); @@ -1056,6 +1067,14 @@ DWBAbandonRef(DWBPendingRef *pref) &batch->slots_written_bitmap[ref.slot_idx / 64]; bool had_owner = (pref->owner != NULL); + /* + * Idempotent: the exit backstop (before_shmem_exit) and a later + * ResourceOwner release may both reach the same entry — the relative + * order of exit callbacks is not fixed across process types. + */ + if (!pref->in_use) + return; + pref->owner = NULL; pref->in_use = false; nPendingRefs--; @@ -1066,8 +1085,8 @@ DWBAbandonRef(DWBPendingRef *pref) if (!(pg_atomic_read_u64(word) & bit)) { /* - * Copy never published: poison the slot so the seal-waiter wakes - * up and recovery ignores it. + * Copy never published: poison the slot so the seal-waiter wakes up + * and recovery ignores it. */ batch->slot_flags[ref.slot_idx] |= DWB_SLOT_ABORTED; pg_write_barrier(); @@ -1078,25 +1097,25 @@ DWBAbandonRef(DWBPendingRef *pref) pg_atomic_read_u32(&batch->state) >= DWB_FSYNCED) { /* - * Copy published and the batch is durable, which means the writer - * was at or past step 6: its smgrwrite may have failed halfway. - * Make the data page whole again from the batch copy. + * Copy published and the batch is durable, which means the writer was + * at or past step 6: its smgrwrite may have failed halfway. Make the + * data page whole again from the batch copy. * - * Only for refs that were attached to a ResourceOwner: those are - * real write-path refs, and their BM_IO_IN_PROGRESS is still held - * here (on the proc-exit path too, see DWBProcExit). An ownerless - * (test) ref never had the buffer-IO interlock, so the repair write - * would race a concurrent flush of the same page. + * Only for refs that were attached to a ResourceOwner: those are real + * write-path refs, and their BM_IO_IN_PROGRESS is still held here (on + * the proc-exit path too, see DWBProcExit). An ownerless (test) ref + * never had the buffer-IO interlock, so the repair write would race a + * concurrent flush of the same page. */ DWBRewriteAbandonedSlot(&ref); } /* * The last ref finishes the batch only once it is FSYNCED. A sealed - * batch cannot lose its last ref earlier — the leader holds its own - * pin from SEAL to FSYNCED (see DWBSealBatch) — so reaching zero refs - * in an earlier state means the batch is not sealed yet: it stays open - * and a later seal completes it normally. + * batch cannot lose its last ref earlier — the leader holds its own pin + * from SEAL to FSYNCED (see DWBSealBatch) — so reaching zero refs in an + * earlier state means the batch is not sealed yet: it stays open and a + * later seal completes it normally. */ if (pg_atomic_fetch_sub_u32(&batch->ref_count, 1) == 1 && pg_atomic_read_u32(&batch->state) == DWB_FSYNCED) @@ -1113,7 +1132,13 @@ ResOwnerReleaseDWBRef(Datum res) } /* - * Process-exit backstop for refs that no ResourceOwner released. + * Exit backstop for refs that no ResourceOwner released. Runs as a + * before_shmem_exit callback: the PGPROC is still alive, so the LWLocks + * taken by a last-ref publication (publish_lock, DWBSegHashLock) are legal + * here — unlike in on_proc_exit callbacks, which run after ProcKill. It + * may run BEFORE the ResourceOwner release of the same refs (callback + * registration order); DWBAbandonRef is idempotent, so whichever side runs + * second is a no-op. * * An owned ref can only get here when abort cleanup was cut short before * the ResourceOwner release phase (e.g. a FATAL thrown out of the abort diff --git a/src/backend/storage/dwb/dwb_file.c b/src/backend/storage/dwb/dwb_file.c index 8647d336420f0..0b3b66d5eea78 100644 --- a/src/backend/storage/dwb/dwb_file.c +++ b/src/backend/storage/dwb/dwb_file.c @@ -297,18 +297,34 @@ DWBPrepareBatchWrite(int batch_idx) * Read one slot's page image back from a batch file. Used by the abort * cleanup of a published ref whose batch is already durable (>= FSYNCED): * the staged copy in shmem is gone by then, the batch file is the - * authoritative source. Failure is PANIC — the caller is about to repair a - * possibly-torn data page and has no fallback. + * authoritative source. Failure — the open included — is PANIC: the + * caller is about to repair a possibly-torn data page, has no fallback, + * and may be running from a ResourceOwner release callback. + * + * Deliberately avoids the VFD layer (BasicOpenFile + raw pg_pread): a + * release callback must not fail, and PathNameOpenFile can throw ERROR + * from its internal allocations. BasicOpenFile allocates nothing and + * still recovers from EMFILE/ENFILE by closing LRU VFDs. */ void DWBReadSlotImage(int batch_idx, int slot_idx, char *dst) { - File file = DWBOpenBatchFile(batch_idx); + char path[MAXPGPATH]; + int fd; off_t off = DWBMetaRegionSize(dwb_batch_pages) + (off_t) slot_idx * BLCKSZ; ssize_t r; - r = FileRead(file, dst, BLCKSZ, off, WAIT_EVENT_DWB_BATCH_READ); + DWBBatchFilePath(path, batch_idx); + fd = BasicOpenFile(path, O_RDONLY | PG_BINARY); + if (fd < 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", path))); + + pgstat_report_wait_start(WAIT_EVENT_DWB_BATCH_READ); + r = pg_pread(fd, dst, BLCKSZ, off); + pgstat_report_wait_end(); if (r != BLCKSZ) { if (r < 0) @@ -321,6 +337,10 @@ DWBReadSlotImage(int batch_idx, int slot_idx, char *dst) errmsg("could not read slot %d of batch %d in \"%s\": read %zd of %d", slot_idx, batch_idx, DWB_DIR, r, BLCKSZ))); } + if (close(fd) != 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", path))); } /* diff --git a/src/backend/storage/dwb/dwb_recovery.c b/src/backend/storage/dwb/dwb_recovery.c index 039acab14c53c..76fe20f5b60fc 100644 --- a/src/backend/storage/dwb/dwb_recovery.c +++ b/src/backend/storage/dwb/dwb_recovery.c @@ -54,8 +54,8 @@ DWBStartup(void) { /* * Geometry GUCs define the on-disk layout. Re-creating the ring - * under a changed geometry must not skip the apply-pass over the - * old ring, so it is deferred to Stage 4; until then, refuse. + * under a changed geometry must not skip the apply-pass over the old + * ring, so it is deferred to Stage 4; until then, refuse. */ ereport(FATAL, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), diff --git a/src/backend/storage/dwb/dwb_retire.c b/src/backend/storage/dwb/dwb_retire.c index 40160c0eac1d1..05555ea7f3a08 100644 --- a/src/backend/storage/dwb/dwb_retire.c +++ b/src/backend/storage/dwb/dwb_retire.c @@ -308,9 +308,11 @@ DWBSegSnapBegin(const DWSegRef *seg) { DWSegEntry *entry; - /* overwriting a leftover snapshot (an fsync that errored out between + /* + * overwriting a leftover snapshot (an fsync that errored out between * Begin and End) is a correct drop: its bits were never cleared and a - * later fsyncer covers them; see also DWBSegmentFsyncBegin */ + * later fsyncer covers them; see also DWBSegmentFsyncBegin + */ seg_sync_snap.active = true; seg_sync_snap.seg = *seg; seg_sync_snap.npairs = 0; @@ -333,9 +335,9 @@ DWBSegSnapBegin(const DWSegRef *seg) seg_sync_snap.pairs[seg_sync_snap.npairs].batch_idx = idx; /* - * Racy read of a 64-bit batch_id outside the publish_lock: - * a torn or stale value only makes the guarded re-check - * below skip the decrement, never decrement a wrong batch. + * Racy read of a 64-bit batch_id outside the publish_lock: a + * torn or stale value only makes the guarded re-check below + * skip the decrement, never decrement a wrong batch. */ seg_sync_snap.pairs[seg_sync_snap.npairs].batch_id = DWBCtl->batches[idx].batch_id; @@ -428,13 +430,13 @@ DWBSegmentFsyncBegin(const FileTag *ftag) /* * Drop any leftover snapshot BEFORE deciding whether to take a new one. - * If a previous fsync ERROR'ed out between Begin and End (possible in - * the checkpointer with data_sync_retry = on, which survives the ERROR - * and keeps this process-local state), the early return below would - * otherwise leave the stale snapshot armed, and the End of the next - * successful fsync of an unrelated non-MD tag would decrement the stale - * segment's back-references -- freeing batches whose data-file fsync - * never succeeded. + * If a previous fsync ERROR'ed out between Begin and End (possible in the + * checkpointer with data_sync_retry = on, which survives the ERROR and + * keeps this process-local state), the early return below would otherwise + * leave the stale snapshot armed, and the End of the next successful + * fsync of an unrelated non-MD tag would decrement the stale segment's + * back-references -- freeing batches whose data-file fsync never + * succeeded. */ seg_sync_snap.active = false; @@ -505,8 +507,8 @@ DWBRetireSegment(const DWSegRef *seg) /* * DWBRetireSyncSegment does not throw on a soft (data_sync_retry = on) - * fsync failure, so the claim reset below always runs; on covered = - * false the snapshot is discarded and the bits stay for a retry. + * fsync failure, so the claim reset below always runs; on covered = false + * the snapshot is discarded and the bits stay for a retry. */ DWBSegSnapBegin(seg); covered = DWBRetireSyncSegment(seg, false); @@ -614,15 +616,24 @@ void DWBRetireWorkersRegister(void) { BackgroundWorker bgw; + int free_slots; if (!DWBIsEnabled() || dwb_retire_workers == 0) return; - if (dwb_retire_workers > max_worker_processes) + /* + * RegisterBackgroundWorker only LOGs on overflow, so check the slots + * actually left after the earlier internal registrations (the logical + * replication launcher above all) and fail loudly: a silently missing + * retire worker would ship a smaller pool than the operator configured. + */ + free_slots = max_worker_processes - GetNumRegisteredBackgroundWorkers(); + if (dwb_retire_workers > free_slots) ereport(FATAL, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"dwb_retire_workers\" (%d) must not exceed \"max_worker_processes\" (%d)", - dwb_retire_workers, max_worker_processes))); + errmsg("\"dwb_retire_workers\" (%d) needs more \"max_worker_processes\" slots than remain free (%d)", + dwb_retire_workers, free_slots), + errhint("Increase \"max_worker_processes\" or decrease \"dwb_retire_workers\"."))); for (int i = 0; i < dwb_retire_workers; i++) { @@ -685,10 +696,10 @@ DWBRetireWorkerMain(Datum main_arg) /* * Force-SEAL pass: writers waiting on a half-filled batch seal it - * themselves after the same timeout, so this only matters for - * batches whose writers all went away before sealing. open_time - * is read unlocked; a torn read can only mis-time the seal, which - * is always a valid action on a non-empty ALLOCATED batch. + * themselves after the same timeout, so this only matters for batches + * whose writers all went away before sealing. open_time is read + * unlocked; a torn read can only mis-time the seal, which is always a + * valid action on a non-empty ALLOCATED batch. */ now = GetCurrentTimestamp(); timeout = dwb_retire_interval_ms; diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c index d513d3e240f69..98524ee17f284 100644 --- a/src/backend/utils/activity/pgstat_io.c +++ b/src/backend/utils/activity/pgstat_io.c @@ -129,14 +129,20 @@ pgstat_count_io_op_time(IOObject io_object, IOContext io_context, IOOp io_op, INSTR_TIME_SET_CURRENT(io_time); INSTR_TIME_SUBTRACT(io_time, start_time); - if (io_object != IOOBJECT_WAL) + /* + * pg_stat_database's blk_read_time/blk_write_time count data-block IO + * only: relation and temp-relation objects. WAL and double write + * buffer IO have their own accounting, and counting the DWB copy of a + * page here would double the apparent block write time. + */ + if (io_object == IOOBJECT_RELATION || io_object == IOOBJECT_TEMP_RELATION) { if (io_op == IOOP_WRITE || io_op == IOOP_EXTEND) { pgstat_count_buffer_write_time(INSTR_TIME_GET_MICROSEC(io_time)); if (io_object == IOOBJECT_RELATION) INSTR_TIME_ADD(pgBufferUsage.shared_blk_write_time, io_time); - else if (io_object == IOOBJECT_TEMP_RELATION) + else INSTR_TIME_ADD(pgBufferUsage.local_blk_write_time, io_time); } else if (io_op == IOOP_READ) @@ -144,7 +150,7 @@ pgstat_count_io_op_time(IOObject io_object, IOContext io_context, IOOp io_op, pgstat_count_buffer_read_time(INSTR_TIME_GET_MICROSEC(io_time)); if (io_object == IOOBJECT_RELATION) INSTR_TIME_ADD(pgBufferUsage.shared_blk_read_time, io_time); - else if (io_object == IOOBJECT_TEMP_RELATION) + else INSTR_TIME_ADD(pgBufferUsage.local_blk_read_time, io_time); } } diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h index 058667a47a0a1..f897b7f093822 100644 --- a/src/include/postmaster/bgworker.h +++ b/src/include/postmaster/bgworker.h @@ -114,6 +114,9 @@ typedef struct BackgroundWorkerHandle BackgroundWorkerHandle; /* Register a new bgworker during shared_preload_libraries */ extern void RegisterBackgroundWorker(BackgroundWorker *worker); +/* How many static bgworkers have been registered so far (postmaster only) */ +extern int GetNumRegisteredBackgroundWorkers(void); + /* Register a new bgworker from a regular backend */ extern bool RegisterDynamicBackgroundWorker(BackgroundWorker *worker, BackgroundWorkerHandle **handle); diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 51c6889c8e298..9f76ec477efc8 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -108,8 +108,8 @@ typedef struct DWBControlFileData uint32 min_version; uint32 num_batches; uint32 batch_pages; - uint64 generation; /* apply-pass horizon: bumped durably on - * every start before the ring opens */ + uint64 generation; /* apply-pass horizon: bumped durably on every + * start before the ring opens */ pg_crc32c crc; /* CRC of all preceding fields */ } DWBControlFileData; @@ -244,8 +244,8 @@ typedef struct DWBatchCtl { pg_atomic_uint32 state; /* DWBatchState */ pg_atomic_uint32 next_slot_idx; /* fetch_add on ALLOCATED */ - pg_atomic_uint32 capped_slots; /* fixed by SEAL; leader waits for - * exactly this many bitmap bits */ + pg_atomic_uint32 capped_slots; /* fixed by SEAL; leader waits for exactly + * this many bitmap bits */ pg_atomic_uint64 slots_written_bitmap[DWB_BITMAP_WORDS]; pg_atomic_uint32 ref_count; /* writers holding the batch from slot * reservation to smgrwrite done */ @@ -292,9 +292,9 @@ typedef struct DWCtl uint64 ring_generation; /* = control.generation after the startup * bump; constant until restart, stamped * into DWSlotMeta by the leader */ - pg_atomic_uint64 freed_events; /* monotonic count of batches that - * reached FREE; backpressure waiters - * treat a change as retire progress */ + pg_atomic_uint64 freed_events; /* monotonic count of batches that reached + * FREE; backpressure waiters treat a + * change as retire progress */ ConditionVariable cv_free_batch; /* broadcast on retire */ ConditionVariable cv_retire_wake; /* wakes retire workers */ slock_t staging_lock; /* protects staging_free bitmap */ @@ -334,6 +334,7 @@ extern void DWBReleaseSlot(const DWBSlotRef *ref); extern bool DWBForceSealOpenBatch(int wclass); extern bool DWBTrySealBatch(int batch_idx); extern DWBatchState DWBGetBatchState(int batch_idx); + /* internal; exported for test_dwb's stale-open regression test */ extern void DWBOpenNewBatch(int wclass, uint32 old_idx); diff --git a/src/test/modules/test_dwb/t/001_dwb.pl b/src/test/modules/test_dwb/t/001_dwb.pl index ba7b33f47fd9a..3243ba0ea5529 100644 --- a/src/test/modules/test_dwb/t/001_dwb.pl +++ b/src/test/modules/test_dwb/t/001_dwb.pl @@ -142,6 +142,19 @@ sub flip_byte $node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=16/, 'ring fully idle after the orphan hand-off'); +# A backend dies holding the LAST ref of an already-durable batch: the exit +# backstop itself performs the FSYNCED -> RETIRING hand-off (seg_set +# publication under LWLocks), which is only legal because it runs as a +# before_shmem_exit callback while the PGPROC is still alive. +$bg = $node->background_psql('postgres'); +$bg->query_safe('SELECT test_dwb_leak_fsynced()'); +$bg->quit; +$node->poll_query_until('postgres', + "SELECT test_dwb_states() LIKE '%retiring=1%'") + or die 'timed out waiting for the exit-time publication'; +is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), + '1', 'batch published from the exit backstop retires'); + # --- transaction abort releases refs (ResourceOwner path) --------------- # An ERROR with unpublished refs: the abort poisons the slots, and the @@ -253,6 +266,31 @@ sub flip_byte $node->start; $node->stop; +# --- the retire worker pool must actually fit into the worker slots ------ + +# The logical replication launcher takes a slot before the pool registers; +# RegisterBackgroundWorker itself only LOGs on overflow, so the pool checks +# the remaining capacity and refuses to start a silently smaller pool. +$log_offset = -s $node->logfile; +$node->append_conf( + 'postgresql.conf', qq( +max_worker_processes = 1 +dwb_retire_workers = 1 +)); +$ret = $node->start(fail_ok => 1); +is($ret, 0, 'start refused when the pool does not fit into worker slots'); +ok( $node->log_contains( + 'needs more "max_worker_processes" slots than remain free', + $log_offset), + 'worker slot shortage reported'); +$node->append_conf( + 'postgresql.conf', qq( +max_worker_processes = 8 +dwb_retire_workers = 0 +)); +$node->start; +$node->stop; + # --- double_writes requires data checksums ----------------------------- my $node2 = PostgreSQL::Test::Cluster->new('dwb_nochecksums'); diff --git a/src/test/modules/test_dwb/t/004_retire_paths.pl b/src/test/modules/test_dwb/t/004_retire_paths.pl index 1c940c4245339..569cc6a76505e 100644 --- a/src/test/modules/test_dwb/t/004_retire_paths.pl +++ b/src/test/modules/test_dwb/t/004_retire_paths.pl @@ -88,4 +88,57 @@ or die 'timed out waiting for the ring to drain after the hash overflow'; pass('ring drained after the hash overflow'); +# --- a leftover fsync snapshot must not be consumed by a foreign End ----- + +# test_dwb_stale_snapshot replays the checkpointer hazard: Begin for the +# parked segment without the matching End (the state an fsync ERROR under +# data_sync_retry = on leaves behind), then a successful Begin/End of an +# unrelated non-MD entry. The parked batch must still be RETIRING — a +# consumed stale snapshot would have freed it without durability. +$node->safe_psql('postgres', 'SELECT test_dwb_park(98000)'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/retiring=1/, 'batch parked for the stale-snapshot scenario'); +$node->safe_psql('postgres', 'SELECT test_dwb_stale_snapshot(98000)'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/retiring=1/, 'stale snapshot dropped, parked batch still RETIRING'); +$node->poll_query_until('postgres', + "SELECT CASE WHEN test_dwb_retire() >= 0 THEN " + . "test_dwb_states() LIKE 'free=64 %' END") + or die 'timed out waiting for the stale-snapshot batch to drain'; + +# --- a soft retire-fsync failure keeps the batch and releases the claim -- + +# With data_sync_retry = on a failed segment fsync must not throw: the +# batch stays RETIRING for a later retry and the advisory claim is +# released. A directory planted at the fake segment's path makes the +# fsync fail deterministically (EISDIR); removing it lets the next sweep +# cover the segment — which only works if the failed attempt released the +# claim. +$node->append_conf('postgresql.conf', 'data_sync_retry = on'); +$node->restart; + +my $segdir = $node->data_dir . '/base/1/99000'; +mkdir $segdir or die "mkdir $segdir: $!"; + +$node->safe_psql('postgres', 'SELECT test_dwb_park(99000)'); +($rc, $out, $err) = $node->psql('postgres', 'SELECT test_dwb_retire()'); +is($rc, 0, 'retire sweep survives the failing segment fsync'); +is($out, '0', 'no batch freed while the segment fsync fails'); +like( + $err, + qr/could not fsync file/, + 'soft fsync failure reported as a WARNING'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/retiring=1/, 'batch stays RETIRING after the soft fsync failure'); + +rmdir $segdir or die "rmdir $segdir: $!"; +is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), + '1', 'released claim lets the next sweep cover the segment'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/^free=64 /, 'ring drained after the soft-failure scenario'); + done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index b0c78ec56d27c..be082371a4221 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -62,3 +62,15 @@ CREATE FUNCTION test_dwb_checkpoint_pending(relnumber oid) CREATE FUNCTION test_dwb_fill_segments(nbatches int) RETURNS int STRICT AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_leak_fsynced() + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_park(relnumber oid) + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_stale_snapshot(relnumber oid) + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index 7a5804b9a4ac0..09d7faf31e7cd 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -27,6 +27,7 @@ #include "storage/dwb.h" #include "storage/fd.h" #include "storage/smgr.h" +#include "storage/sync.h" #include "utils/builtins.h" PG_MODULE_MAGIC; @@ -190,8 +191,8 @@ count_ring_slots(bool current_only, bool have_filter, Oid relnumber) /* * A CRC-valid header with out-of-range n_slots cannot happen - * under the startup geometry check; report the anomaly instead - * of silently contributing zero slots. + * under the startup geometry check; report the anomaly instead of + * silently contributing zero slots. */ if (hdr.n_slots > control.batch_pages) { @@ -602,6 +603,111 @@ test_dwb_checkpoint_pending(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } +/* + * Acquire one ownerless ref, publish, seal and wait until the batch is + * durable, then return WITHOUT releasing: closing the session leaves the + * exit backstop holding the LAST ref of a DWB_FSYNCED batch, so the + * FSYNCED -> RETIRING hand-off (seg_set publication under publish_lock and + * DWBSegHashLock) runs inside the exit callback itself. This is only + * legal from before_shmem_exit, while the PGPROC is still alive. + */ +PG_FUNCTION_INFO_V1(test_dwb_leak_fsynced); +Datum +test_dwb_leak_fsynced(PG_FUNCTION_ARGS) +{ + BufferTag tag; + DWBSlotRef ref; + RelFileLocator rlocator; + static char page[BLCKSZ]; + + check_dwb_enabled(); + + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = 1; + rlocator.relNumber = 97000; + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); + + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); + memset(page, 'E', BLCKSZ); + DWBPublishImage(&ref, page, (XLogRecPtr) 0x8000000); + if (!DWBTrySealBatch(ref.batch_idx)) + ereport(ERROR, (errmsg("could not seal the batch under test"))); + DWBWaitBatchFsynced(&ref); + PG_RETURN_VOID(); +} + +/* + * Park one batch in RETIRING on a single fake segment (relnumber, block 0, + * database oid 1): acquire, publish, seal, wait durable, release. The + * batch stays RETIRING until something fsyncs the segment (the fake + * relation makes that an ENOENT = covered, unless the test planted a real + * obstacle at the segment path). + */ +PG_FUNCTION_INFO_V1(test_dwb_park); +Datum +test_dwb_park(PG_FUNCTION_ARGS) +{ + Oid relnumber = PG_GETARG_OID(0); + BufferTag tag; + DWBSlotRef ref; + RelFileLocator rlocator; + static char page[BLCKSZ]; + + check_dwb_enabled(); + + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = 1; + rlocator.relNumber = relnumber; + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); + + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); + memset(page, 'P', BLCKSZ); + DWBPublishImage(&ref, page, (XLogRecPtr) 0x9000000); + if (!DWBTrySealBatch(ref.batch_idx)) + ereport(ERROR, (errmsg("could not seal the batch under test"))); + DWBWaitBatchFsynced(&ref); + DWBReleaseSlot(&ref); /* last ref: publication, RETIRING */ + PG_RETURN_VOID(); +} + +/* + * Replay the checkpointer's stale-snapshot hazard against a parked batch: + * DWBSegmentFsyncBegin for the parked segment WITHOUT the matching End + * (exactly the state an fsync ERROR under data_sync_retry = on leaves + * behind), then a successful Begin/End of an unrelated non-MD sync entry. + * The leftover snapshot must be dropped, not consumed: the parked batch + * has to stay RETIRING. + */ +PG_FUNCTION_INFO_V1(test_dwb_stale_snapshot); +Datum +test_dwb_stale_snapshot(PG_FUNCTION_ARGS) +{ + Oid relnumber = PG_GETARG_OID(0); + FileTag md_tag; + FileTag clog_tag; + + check_dwb_enabled(); + + memset(&md_tag, 0, sizeof(md_tag)); + md_tag.handler = SYNC_HANDLER_MD; + md_tag.rlocator.spcOid = DEFAULTTABLESPACE_OID; + md_tag.rlocator.dbOid = 1; + md_tag.rlocator.relNumber = relnumber; + md_tag.forknum = MAIN_FORKNUM; + md_tag.segno = 0; + + /* arm the snapshot; no End, as if the fsync threw an ERROR */ + DWBSegmentFsyncBegin(&md_tag); + + /* an unrelated non-MD entry syncs successfully */ + memset(&clog_tag, 0, sizeof(clog_tag)); + clog_tag.handler = SYNC_HANDLER_CLOG; + DWBSegmentFsyncBegin(&clog_tag); + (void) DWBSegmentFsyncEnd(true); + + PG_RETURN_VOID(); +} + /* * Publish nbatches full batches whose slots all point at DISTINCT fake * segments, so that RETIRING batches accumulate DWSegmentHash entries until diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cd74c4e5b6eb..67dc8a537b15a 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -600,7 +600,21 @@ DR_sqlfunction DR_transientrel DSMRegistryCtxStruct DSMRegistryEntry +DWBBatchHeader +DWBControlFileData +DWBOnStall +DWBPendingRef +DWBSegSyncSnap +DWBSlotRef +DWBStallState +DWBTornPageProtection +DWBatchCtl +DWBatchState +DWCtl DWORD +DWSegEntry +DWSegRef +DWSlotMeta DataDirSyncMethod DataDumperPtr DataPageDeleteStack From d813a0f7eaafbcc15f9f9228e92406bef81cb0d2 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 24 Jul 2026 20:19:15 +0300 Subject: [PATCH 08/52] Simplify after the Stage 2 review round A complexity-focused review pass over the Stage 2 commits; behavior is unchanged and the full test_dwb suite stays green. * Drop the nPendingRefs counter: the free-slot scan in DWBAcquireSlot already knows whether the array is full, so the overflow guard moves after the scan and the counter bookkeeping disappears. * Replace the hand-rolled bubble sort of RETIRING batches in DWBRetireSweep with qsort over a named DWBRetiringBatch struct and a pg_cmp_u64 comparator; batch ids are unique, so stability is not needed, and O(n^2) was pointless on 1024-batch rings. * Deduplicate the test harness: make_tag() replaces nine copies of the RelFileLocator/InitBufferTag boilerplate, stage_one_page() is the shared acquire-publish-seal-wait prologue of the five single-page scenarios, and leak_refs() is the shared body of the leak and abort-release scenarios. Upcoming checkpoint and apply-pass tests can reuse these instead of growing new copies. Deliberately kept: the dwb-after-batch-fsynced injection point (the hook for future apply-pass crash tests between the batch fsync and smgrwrite) and DWBWriterClass() (the seam where flush-source classification will grow). --- src/backend/storage/dwb/dwb.c | 12 +- src/backend/storage/dwb/dwb_retire.c | 36 ++--- src/test/modules/test_dwb/test_dwb.c | 207 ++++++++++----------------- src/tools/pgindent/typedefs.list | 1 + 4 files changed, 97 insertions(+), 159 deletions(-) diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 1b80163f537cf..95dd901d841e0 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -60,7 +60,6 @@ typedef struct DWBPendingRef } DWBPendingRef; static DWBPendingRef pendingRefs[2 * DWB_BATCH_MAX_PAGES]; -static int nPendingRefs = 0; static bool cleanup_registered = false; /* leader-side meta assembly area, allocated before the seal is attempted */ @@ -642,10 +641,6 @@ DWBAcquireSlot(const BufferTag *tag, int wclass, bool use_resowner, Assert(DWBIsEnabled()); Assert(wclass >= 0 && wclass < DWB_NUM_WCLASSES); - /* hard bound: overflowing the static array would corrupt memory */ - if (nPendingRefs >= (int) lengthof(pendingRefs)) - elog(ERROR, "too many pending double write buffer slot refs held by one backend"); - for (int i = 0; i < (int) lengthof(pendingRefs); i++) { if (!pendingRefs[i].in_use) @@ -654,7 +649,9 @@ DWBAcquireSlot(const BufferTag *tag, int wclass, bool use_resowner, break; } } - Assert(pref != NULL); + /* hard bound: overflowing the static array would corrupt memory */ + if (pref == NULL) + elog(ERROR, "too many pending double write buffer slot refs held by one backend"); /* no failure window between the reservation below and remembering it */ if (use_resowner) @@ -747,7 +744,6 @@ DWBAcquireSlot(const BufferTag *tag, int wclass, bool use_resowner, pref->ref = *ref; pref->owner = use_resowner ? CurrentResourceOwner : NULL; pref->in_use = true; - nPendingRefs++; if (pref->owner) ResourceOwnerRemember(pref->owner, PointerGetDatum(pref), &dwb_ref_resowner_desc); @@ -859,7 +855,6 @@ DWBReleaseSlot(const DWBSlotRef *ref) &dwb_ref_resowner_desc); pref->owner = NULL; pref->in_use = false; - nPendingRefs--; break; } } @@ -1077,7 +1072,6 @@ DWBAbandonRef(DWBPendingRef *pref) pref->owner = NULL; pref->in_use = false; - nPendingRefs--; /* a held ref pins the batch, so its incarnation cannot have changed */ Assert(ref.batch_id == batch->batch_id); diff --git a/src/backend/storage/dwb/dwb_retire.c b/src/backend/storage/dwb/dwb_retire.c index 05555ea7f3a08..e528988d048d6 100644 --- a/src/backend/storage/dwb/dwb_retire.c +++ b/src/backend/storage/dwb/dwb_retire.c @@ -39,6 +39,7 @@ #include "postgres.h" #include "common/hashfn.h" +#include "common/int.h" #include "miscadmin.h" #include "port/pg_bitutils.h" #include "postmaster/bgworker.h" @@ -543,19 +544,28 @@ DWBRetireAllSync(void) return DWBRetireSweep(-1); } +typedef struct DWBRetiringBatch +{ + int idx; + uint64 id; +} DWBRetiringBatch; + +static int +dwb_retiring_batch_cmp(const void *a, const void *b) +{ + return pg_cmp_u64(((const DWBRetiringBatch *) a)->id, + ((const DWBRetiringBatch *) b)->id); +} + static int DWBRetireSweep(int worker_id) { - struct - { - int idx; - uint64 id; - } *retiring; + DWBRetiringBatch *retiring; int nretiring = 0; int freed = 0; DWSegRef *segs; - retiring = palloc(dwb_num_batches * sizeof(*retiring)); + retiring = palloc(dwb_num_batches * sizeof(DWBRetiringBatch)); segs = palloc(dwb_batch_pages * sizeof(DWSegRef)); for (int i = 0; i < dwb_num_batches; i++) @@ -569,18 +579,8 @@ DWBRetireSweep(int worker_id) } /* oldest first: smaller batch_id was opened earlier */ - for (int i = 0; i < nretiring; i++) - for (int j = i + 1; j < nretiring; j++) - if (retiring[j].id < retiring[i].id) - { - uint64 tid = retiring[i].id; - int tidx = retiring[i].idx; - - retiring[i].id = retiring[j].id; - retiring[i].idx = retiring[j].idx; - retiring[j].id = tid; - retiring[j].idx = tidx; - } + qsort(retiring, nretiring, sizeof(DWBRetiringBatch), + dwb_retiring_batch_cmp); for (int i = 0; i < nretiring; i++) { diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index 09d7faf31e7cd..fc3b26d3044a0 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -51,6 +51,62 @@ wait_and_release(DWBSlotRef *refs, int nrefs) } } +/* build a MAIN_FORKNUM page tag for (relnumber, blkno) in database dboid */ +static BufferTag +make_tag(Oid dboid, Oid relnumber, BlockNumber blkno) +{ + BufferTag tag; + RelFileLocator rlocator; + + rlocator.spcOid = DEFAULTTABLESPACE_OID; + rlocator.dbOid = dboid; + rlocator.relNumber = relnumber; + InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, blkno); + return tag; +} + +/* + * Acquire one slot for the tag, publish the image, seal the batch and wait + * until it is durable: the shared prologue of the single-page scenarios. + */ +static void +stage_one_page(const BufferTag *tag, const char *image, XLogRecPtr page_lsn, + bool use_resowner, DWBSlotRef *ref) +{ + DWBAcquireSlot(tag, DWB_WCLASS_EVICTION, use_resowner, ref); + DWBPublishImage(ref, image, page_lsn); + if (!DWBTrySealBatch(ref->batch_idx)) + ereport(ERROR, (errmsg("could not seal the batch under test"))); + DWBWaitBatchFsynced(ref); +} + +/* + * Acquire (and optionally publish) npages slots and return with the refs + * still pending: the shared body of the leak / abort-release scenarios. + */ +static void +leak_refs(int npages, bool do_publish, bool use_resowner, Oid relnumber) +{ + static char page[BLCKSZ]; + + /* stay below the batch size so this backend never seals as leader */ + if (npages < 1 || npages >= dwb_batch_pages) + ereport(ERROR, (errmsg("npages out of range"))); + + for (int i = 0; i < npages; i++) + { + BufferTag tag = make_tag(1, relnumber, (BlockNumber) i); + DWBSlotRef ref; + + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, use_resowner, &ref); + if (do_publish) + { + memset(page, 'L', BLCKSZ); + DWBPublishImage(&ref, page, (XLogRecPtr) 0x2000000 + i); + } + } +} + /* * One full write cycle over npages synthetic pages: acquire, publish, * seal (by overflow or forced), wait durable, release, retire. @@ -67,17 +123,11 @@ dwb_cycle_internal(int npages) for (int i = 0; i < npages; i++) { - BufferTag tag; + BufferTag tag = make_tag(1, 90000 + (i % 3), (BlockNumber) i); DWBSlotRef ref; - RelFileLocator rlocator; CHECK_FOR_INTERRUPTS(); - rlocator.spcOid = DEFAULTTABLESPACE_OID; - rlocator.dbOid = 1; - rlocator.relNumber = 90000 + (i % 3); - InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, (BlockNumber) i); - DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); /* @@ -325,31 +375,9 @@ test_dwb_leak(PG_FUNCTION_ARGS) { int npages = PG_GETARG_INT32(0); bool do_publish = PG_GETARG_BOOL(1); - static char page[BLCKSZ]; check_dwb_enabled(); - /* stay below the batch size so this backend never seals as leader */ - if (npages < 1 || npages >= dwb_batch_pages) - ereport(ERROR, (errmsg("npages out of range"))); - - for (int i = 0; i < npages; i++) - { - BufferTag tag; - DWBSlotRef ref; - RelFileLocator rlocator; - - rlocator.spcOid = DEFAULTTABLESPACE_OID; - rlocator.dbOid = 1; - rlocator.relNumber = 91000; - InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, (BlockNumber) i); - - DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); - if (do_publish) - { - memset(page, 'L', BLCKSZ); - DWBPublishImage(&ref, page, (XLogRecPtr) 0x2000000 + i); - } - } + leak_refs(npages, do_publish, false, 91000); PG_RETURN_VOID(); } @@ -381,7 +409,6 @@ test_dwb_fill_ring(PG_FUNCTION_ARGS) uint32 open_idx; BufferTag tag; DWBSlotRef ref; - RelFileLocator rlocator; CHECK_FOR_INTERRUPTS(); @@ -399,10 +426,7 @@ test_dwb_fill_ring(PG_FUNCTION_ARGS) (DWB_SEAL_BIT | DWB_IDX_MASK)) >= (uint32) dwb_batch_pages)) break; /* one more acquire would block */ - rlocator.spcOid = DEFAULTTABLESPACE_OID; - rlocator.dbOid = 1; - rlocator.relNumber = 95000 + (background ? 1000 : 0); - InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, (BlockNumber) taken); + tag = make_tag(1, 95000 + (background ? 1000 : 0), (BlockNumber) taken); DWBAcquireSlot(&tag, wclass, false, &ref); memset(page, 'X', BLCKSZ); @@ -426,30 +450,9 @@ test_dwb_abort_release(PG_FUNCTION_ARGS) { int npages = PG_GETARG_INT32(0); bool do_publish = PG_GETARG_BOOL(1); - static char page[BLCKSZ]; check_dwb_enabled(); - if (npages < 1 || npages >= dwb_batch_pages) - ereport(ERROR, (errmsg("npages out of range"))); - - for (int i = 0; i < npages; i++) - { - BufferTag tag; - DWBSlotRef ref; - RelFileLocator rlocator; - - rlocator.spcOid = DEFAULTTABLESPACE_OID; - rlocator.dbOid = 1; - rlocator.relNumber = 93000; - InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, (BlockNumber) i); - - DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, true, &ref); - if (do_publish) - { - memset(page, 'R', BLCKSZ); - DWBPublishImage(&ref, page, (XLogRecPtr) 0x4000000 + i); - } - } + leak_refs(npages, do_publish, true, 93000); ereport(ERROR, (errmsg("test_dwb: deliberate abort with pending refs"))); PG_RETURN_VOID(); /* unreachable */ @@ -467,25 +470,14 @@ PG_FUNCTION_INFO_V1(test_dwb_abort_after_fsync); Datum test_dwb_abort_after_fsync(PG_FUNCTION_ARGS) { - BufferTag tag; + BufferTag tag = make_tag(1, 94000, 0); DWBSlotRef ref; - RelFileLocator rlocator; static char page[BLCKSZ]; check_dwb_enabled(); - rlocator.spcOid = DEFAULTTABLESPACE_OID; - rlocator.dbOid = 1; - rlocator.relNumber = 94000; - InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); - - DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, true, &ref); memset(page, 'F', BLCKSZ); - DWBPublishImage(&ref, page, (XLogRecPtr) 0x5000000); - - if (!DWBTrySealBatch(ref.batch_idx)) - ereport(ERROR, (errmsg("could not seal the batch under test"))); - DWBWaitBatchFsynced(&ref); + stage_one_page(&tag, page, (XLogRecPtr) 0x5000000, true, &ref); ereport(ERROR, (errmsg("test_dwb: deliberate abort after batch fsync"))); PG_RETURN_VOID(); /* unreachable */ @@ -505,9 +497,8 @@ test_dwb_torn_repair(PG_FUNCTION_ARGS) { Oid relnumber = PG_GETARG_OID(0); BlockNumber blkno = (BlockNumber) PG_GETARG_INT32(1); - BufferTag tag; + BufferTag tag = make_tag(MyDatabaseId, relnumber, blkno); DWBSlotRef ref; - RelFileLocator rlocator; RelPathStr relpath; static PGAlignedBlock image; static char junk[BLCKSZ / 2]; @@ -515,12 +506,7 @@ test_dwb_torn_repair(PG_FUNCTION_ARGS) check_dwb_enabled(); - rlocator.spcOid = DEFAULTTABLESPACE_OID; - rlocator.dbOid = MyDatabaseId; - rlocator.relNumber = relnumber; - InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, blkno); - - relpath = relpathperm(rlocator, MAIN_FORKNUM); + relpath = relpathperm(BufTagGetRelFileLocator(&tag), MAIN_FORKNUM); fd = OpenTransientFile(relpath.str, O_RDWR | PG_BINARY); if (fd < 0) ereport(ERROR, @@ -534,11 +520,7 @@ test_dwb_torn_repair(PG_FUNCTION_ARGS) blkno, relpath.str))); /* stage the pristine image; the abort below must put it back */ - DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, true, &ref); - DWBPublishImage(&ref, image.data, PageGetLSN((Page) image.data)); - if (!DWBTrySealBatch(ref.batch_idx)) - ereport(ERROR, (errmsg("could not seal the batch under test"))); - DWBWaitBatchFsynced(&ref); + stage_one_page(&tag, image.data, PageGetLSN((Page) image.data), true, &ref); /* simulate a torn smgrwrite: clobber the second half of the block */ memset(junk, 0x7F, sizeof(junk)); @@ -574,27 +556,18 @@ Datum test_dwb_checkpoint_pending(PG_FUNCTION_ARGS) { Oid relnumber = PG_GETARG_OID(0); - BufferTag tag; + BufferTag tag = make_tag(MyDatabaseId, relnumber, 0); DWBSlotRef ref; - RelFileLocator rlocator; SMgrRelation reln; static PGAlignedBlock image; check_dwb_enabled(); - rlocator.spcOid = DEFAULTTABLESPACE_OID; - rlocator.dbOid = MyDatabaseId; - rlocator.relNumber = relnumber; - InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); - - reln = smgropen(rlocator, INVALID_PROC_NUMBER); + reln = smgropen(BufTagGetRelFileLocator(&tag), INVALID_PROC_NUMBER); smgrread(reln, MAIN_FORKNUM, 0, image.data); - DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); - DWBPublishImage(&ref, image.data, PageGetLSN((Page) image.data)); - if (!DWBTrySealBatch(ref.batch_idx)) - ereport(ERROR, (errmsg("could not seal the batch under test"))); - DWBWaitBatchFsynced(&ref); + stage_one_page(&tag, image.data, PageGetLSN((Page) image.data), false, + &ref); /* the data-file write; registers the checkpointer sync request */ smgrwrite(reln, MAIN_FORKNUM, 0, image.data, false); @@ -615,24 +588,14 @@ PG_FUNCTION_INFO_V1(test_dwb_leak_fsynced); Datum test_dwb_leak_fsynced(PG_FUNCTION_ARGS) { - BufferTag tag; + BufferTag tag = make_tag(1, 97000, 0); DWBSlotRef ref; - RelFileLocator rlocator; static char page[BLCKSZ]; check_dwb_enabled(); - rlocator.spcOid = DEFAULTTABLESPACE_OID; - rlocator.dbOid = 1; - rlocator.relNumber = 97000; - InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); - - DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); memset(page, 'E', BLCKSZ); - DWBPublishImage(&ref, page, (XLogRecPtr) 0x8000000); - if (!DWBTrySealBatch(ref.batch_idx)) - ereport(ERROR, (errmsg("could not seal the batch under test"))); - DWBWaitBatchFsynced(&ref); + stage_one_page(&tag, page, (XLogRecPtr) 0x8000000, false, &ref); PG_RETURN_VOID(); } @@ -648,24 +611,14 @@ Datum test_dwb_park(PG_FUNCTION_ARGS) { Oid relnumber = PG_GETARG_OID(0); - BufferTag tag; + BufferTag tag = make_tag(1, relnumber, 0); DWBSlotRef ref; - RelFileLocator rlocator; static char page[BLCKSZ]; check_dwb_enabled(); - rlocator.spcOid = DEFAULTTABLESPACE_OID; - rlocator.dbOid = 1; - rlocator.relNumber = relnumber; - InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); - - DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); memset(page, 'P', BLCKSZ); - DWBPublishImage(&ref, page, (XLogRecPtr) 0x9000000); - if (!DWBTrySealBatch(ref.batch_idx)) - ereport(ERROR, (errmsg("could not seal the batch under test"))); - DWBWaitBatchFsynced(&ref); + stage_one_page(&tag, page, (XLogRecPtr) 0x9000000, false, &ref); DWBReleaseSlot(&ref); /* last ref: publication, RETIRING */ PG_RETURN_VOID(); } @@ -737,13 +690,7 @@ test_dwb_fill_segments(PG_FUNCTION_ARGS) for (int i = 0; i < dwb_batch_pages; i++) { - BufferTag tag; - RelFileLocator rlocator; - - rlocator.spcOid = DEFAULTTABLESPACE_OID; - rlocator.dbOid = 1; - rlocator.relNumber = next_relnumber++; - InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); + BufferTag tag = make_tag(1, next_relnumber++, 0); DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &refs[i]); /* the whole batch must be ours for the seal below to cover it */ @@ -800,7 +747,6 @@ test_dwb_open_stale(PG_FUNCTION_ARGS) uint32 reopened_idx; DWBSlotRef ref; BufferTag tag; - RelFileLocator rlocator; static char page[BLCKSZ]; check_dwb_enabled(); @@ -817,10 +763,7 @@ test_dwb_open_stale(PG_FUNCTION_ARGS) * Acquire one slot: the fetch_add bounces on SEAL_BIT and reopens the * lowest FREE index — the same index again, as a new live incarnation. */ - rlocator.spcOid = DEFAULTTABLESPACE_OID; - rlocator.dbOid = 1; - rlocator.relNumber = 92000; - InitBufferTag(&tag, &rlocator, MAIN_FORKNUM, 0); + tag = make_tag(1, 92000, 0); DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); memset(page, 'S', BLCKSZ); DWBPublishImage(&ref, page, (XLogRecPtr) 0x3000000); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 67dc8a537b15a..279ff418d6c3e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -604,6 +604,7 @@ DWBBatchHeader DWBControlFileData DWBOnStall DWBPendingRef +DWBRetiringBatch DWBSegSyncSnap DWBSlotRef DWBStallState From 40611dce86713e7c5eeaf4bc32c65bffbbb50908 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 24 Jul 2026 21:10:34 +0300 Subject: [PATCH 09/52] Integrate checkpoint, hot standby and base backups (Stage 3) io_torn_pages_protection now drives WAL full-page images through a single derivation point, EffectiveFullPageWrites(): "double_writes" and "off" force FPIs off, while "full_pages" keeps the legacy full_page_writes GUC meaningful (external consumers such as pg_upgrade still pass it). Online backups keep forcing page images through the vanilla runningBackups term of doPageWrites; repairing a page copied mid-write into a backup is FPI replay's job, not the ring's. The pg_dwb ring is local to an instance, so its contents are excluded from base backups via excludeDirContents, which also converts a symlinked pg_dwb into an empty real directory. This is mirrored in pg_rewind's filemap so a rewind never copies the source's ring over; the target's own leftover ring stays inert thanks to the generation bump on every start. Checkpoints take no DWB barrier by design and needed no code; a new 004 scenario pins that a CHECKPOINT completes while an open ALLOCATED batch is live and leaves it alone. New TAP coverage: 005_standby.pl (a standby cold-starts a fresh ring from a base backup, runs its own ring during replay with the retire worker alive in recovery, survives crashes on either side, promotes with a replay backlog and advances minRecoveryPoint past replayed flushes) and 006_backup.pl (pg_waldump shows no FPW outside a backup window and FPWs inside one; the backup keeps pg_dwb as an empty directory with no warnings, including the symlink layout; a restored cluster cold-starts a fresh generation). --- src/backend/access/transam/xlog.c | 28 +++- src/backend/backup/basebackup.c | 9 ++ src/bin/pg_rewind/filemap.c | 8 + src/test/modules/test_dwb/meson.build | 2 + .../modules/test_dwb/t/004_retire_paths.pl | 42 +++++ src/test/modules/test_dwb/t/005_standby.pl | 147 ++++++++++++++++++ src/test/modules/test_dwb/t/006_backup.pl | 145 +++++++++++++++++ 7 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 src/test/modules/test_dwb/t/005_standby.pl create mode 100644 src/test/modules/test_dwb/t/006_backup.pl diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 3e1c57825c580..fbab91461a553 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -688,6 +688,7 @@ static void UpdateLastRemovedPtr(char *filename); static void ValidateXLOGDirectoryStructure(void); static void CleanupBackupHistory(void); static void UpdateMinRecoveryPoint(XLogRecPtr lsn, bool force); +static bool EffectiveFullPageWrites(void); static bool PerformRecoveryXLogAction(void); static void InitControlFile(uint64 sysidentifier, uint32 data_checksum_version); static void WriteControlFile(void); @@ -5116,7 +5117,7 @@ BootStrapXLOG(uint32 data_checksum_version) checkPoint.redo = wal_segment_size + SizeOfXLogLongPHD; checkPoint.ThisTimeLineID = BootstrapTimeLineID; checkPoint.PrevTimeLineID = BootstrapTimeLineID; - checkPoint.fullPageWrites = fullPageWrites; + checkPoint.fullPageWrites = EffectiveFullPageWrites(); checkPoint.wal_level = wal_level; checkPoint.nextXid = FullTransactionIdFromEpochAndXid(0, FirstNormalTransactionId); @@ -8211,6 +8212,22 @@ XLogReportParameters(void) } } +/* + * Whether WAL records should carry full-page images. + * + * io_torn_pages_protection selects the torn-page protection mechanism: under + * "double_writes" the durable copy in pg_dwb/ replaces FPIs and under "off" + * the user has declared torn writes impossible, so both force this off; the + * legacy full_page_writes GUC keeps its meaning under "full_pages" only. + * Online backups still force page images regardless of this value, through + * the runningBackups term of doPageWrites (see XLogInsertRecord). + */ +static bool +EffectiveFullPageWrites(void) +{ + return io_torn_pages_protection == DWB_PROTECT_FULL_PAGES && fullPageWrites; +} + /* * Update full_page_writes in shared memory, and write an * XLOG_FPW_CHANGE record if necessary. @@ -8222,6 +8239,7 @@ void UpdateFullPageWrites(void) { XLogCtlInsert *Insert = &XLogCtl->Insert; + bool newFullPageWrites = EffectiveFullPageWrites(); bool recoveryInProgress; /* @@ -8231,7 +8249,7 @@ UpdateFullPageWrites(void) * because we assume that there is no concurrently running process which * can update it. */ - if (fullPageWrites == Insert->fullPageWrites) + if (newFullPageWrites == Insert->fullPageWrites) return; /* @@ -8250,7 +8268,7 @@ UpdateFullPageWrites(void) * setting it to false, first write the WAL record and then set the global * flag. */ - if (fullPageWrites) + if (newFullPageWrites) { WALInsertLockAcquireExclusive(); Insert->fullPageWrites = true; @@ -8264,12 +8282,12 @@ UpdateFullPageWrites(void) if (XLogStandbyInfoActive() && !recoveryInProgress) { XLogBeginInsert(); - XLogRegisterData(&fullPageWrites, sizeof(bool)); + XLogRegisterData(&newFullPageWrites, sizeof(bool)); XLogInsert(RM_XLOG_ID, XLOG_FPW_CHANGE); } - if (!fullPageWrites) + if (!newFullPageWrites) { WALInsertLockAcquireExclusive(); Insert->fullPageWrites = false; diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c index f0f88838dc21a..cd28fee3ea3e1 100644 --- a/src/backend/backup/basebackup.c +++ b/src/backend/backup/basebackup.c @@ -41,6 +41,7 @@ #include "storage/bufpage.h" #include "storage/checksum.h" #include "storage/dsm_impl.h" +#include "storage/dwb.h" #include "storage/ipc.h" #include "storage/reinit.h" #include "utils/builtins.h" @@ -166,6 +167,14 @@ static const char *const excludeDirContents[] = /* Contents removed on startup, see dsm_cleanup_for_mmap(). */ PG_DYNSHMEM_DIR, + /* + * The double write buffer ring only repairs torn writes on the local + * instance; restoring it elsewhere would let the apply-pass overwrite + * pages with copies from the backup moment. A restored cluster starts + * with a fresh ring instead, see DWBStartup(). + */ + DWB_DIR, + /* Contents removed on startup, see AsyncShmemInit(). */ "pg_notify", diff --git a/src/bin/pg_rewind/filemap.c b/src/bin/pg_rewind/filemap.c index c933871ca9fda..812fc8a115747 100644 --- a/src/bin/pg_rewind/filemap.c +++ b/src/bin/pg_rewind/filemap.c @@ -132,6 +132,14 @@ static const char *const excludeDirContents[] = /* Contents removed on startup, see dsm_cleanup_for_mmap(). */ "pg_dynshmem", /* defined as PG_DYNSHMEM_DIR */ + /* + * The double write buffer ring is local to an instance; copying the + * source's ring over would hand the target foreign page copies. The + * target's own leftover ring is inert: the durable generation bump on + * every start (see DWBStartup()) keeps its slots out of any apply-pass. + */ + "pg_dwb", /* defined as DWB_DIR */ + /* Contents removed on startup, see AsyncShmemInit(). */ "pg_notify", diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index 069f3be429561..dcf1736fe023c 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -41,6 +41,8 @@ tests += { 't/002_flushbuffer.pl', 't/003_backpressure.pl', 't/004_retire_paths.pl', + 't/005_standby.pl', + 't/006_backup.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/004_retire_paths.pl b/src/test/modules/test_dwb/t/004_retire_paths.pl index 569cc6a76505e..48149d35305ee 100644 --- a/src/test/modules/test_dwb/t/004_retire_paths.pl +++ b/src/test/modules/test_dwb/t/004_retire_paths.pl @@ -65,6 +65,48 @@ $node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/^free=64 /, 'CHECKPOINT alone retired the parked batch'); +# --- a checkpoint tolerates a live ALLOCATED batch ------------------------ + +# Checkpoints take no DWB barrier: an open batch whose timeout has not +# fired stays ALLOCATED across a CHECKPOINT and is finished asynchronously. +# +# Warmup: a throwaway session runs the same statements once and a +# CHECKPOINT flushes every catalog page its login dirtied (hint bits under +# checksums), so the real holder below leaves no dirty buffer for the +# checkpoint to feed through the DWB write path — which would seal the +# open batch as a side effect. +my $warm = $node->background_psql('postgres'); +$warm->query_safe('SELECT test_dwb_leak(1, true)'); +$warm->quit; +$node->poll_query_until('postgres', + "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " + . "CASE WHEN test_dwb_retire() >= 0 THEN " + . "test_dwb_states() LIKE 'free=64 %' END END") + or die 'timed out draining the warmup batch'; +$node->safe_psql('postgres', 'CHECKPOINT'); +$node->poll_query_until('postgres', + "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " + . "CASE WHEN test_dwb_retire() >= 0 THEN " + . "test_dwb_states() LIKE 'free=64 %' END END") + or die 'timed out draining the warmup checkpoint traffic'; + +my $holder = $node->background_psql('postgres'); +$holder->query_safe('SELECT test_dwb_leak(1, true)'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/allocated=1/, 'an open ALLOCATED batch is live before the checkpoint'); +$node->safe_psql('postgres', 'CHECKPOINT'); +like( + $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/allocated=1/, 'CHECKPOINT completed and left the open batch alone'); +$holder->quit; +$node->poll_query_until('postgres', + "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " + . "CASE WHEN test_dwb_retire() >= 0 THEN " + . "test_dwb_states() LIKE 'free=64 %' END END") + or die 'timed out waiting for the open batch to drain after the holder quit'; +pass('abandoned open batch drained'); + # --- segment hash overflow degrades to synchronous retire ---------------- my $log_offset = -s $node->logfile; diff --git a/src/test/modules/test_dwb/t/005_standby.pl b/src/test/modules/test_dwb/t/005_standby.pl new file mode 100644 index 0000000000000..c7ce937602c86 --- /dev/null +++ b/src/test/modules/test_dwb/t/005_standby.pl @@ -0,0 +1,147 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Hot standby under io_torn_pages_protection = double_writes: the standby +# runs its own ring while replaying, both sides survive crashes, promotion +# works with a replay backlog, and the minRecoveryPoint contract holds for +# pages the standby flushes through the DWB. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $primary = PostgreSQL::Test::Cluster->new('dwb_primary'); +$primary->init(allows_streaming => 1); +# A tiny buffer pool forces replay evictions through the standby's ring; +# fsync must be ON so restartpoint ProcessSyncRequests retires batches. +$primary->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 1 +dwb_batch_timeout_ms = 20 +shared_buffers = 2MB +autovacuum = off +fsync = on +)); +$primary->start; +$primary->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); + +# --- a standby provisioned from a base backup cold-starts its ring ------- + +$primary->backup('bkp'); +my $standby = PostgreSQL::Test::Cluster->new('dwb_standby'); +$standby->init_from_backup($primary, 'bkp', has_streaming => 1); +my $standby_log_offset = -s $standby->logfile; +$standby->start; + +# pg_dwb/ is excluded from the backup, so the standby must open a fresh +# ring rather than inherit the primary's. +ok( $standby->log_contains( + qr/double write buffer ring opened: 16 batches of 16 pages, generation 1/, + $standby_log_offset), + 'standby cold-started a fresh ring from the base backup'); + +# --- the retire worker pool runs during recovery ------------------------- + +$standby->poll_query_until('postgres', + "SELECT count(*) = 1 FROM pg_stat_activity WHERE backend_type = 'dwb retire worker'" +) or die 'timed out waiting for the standby retire worker to start'; +pass('retire worker is running on the standby during recovery'); + +# --- replay traffic flows through the standby ring ----------------------- + +my $mrp_before = $standby->safe_psql('postgres', + 'SELECT min_recovery_end_lsn FROM pg_control_recovery()'); + +$primary->safe_psql('postgres', q( + CREATE TABLE dwb_t AS + SELECT g AS id, repeat('x', 300) AS filler + FROM generate_series(1, 50000) g; + UPDATE dwb_t SET filler = repeat('y', 300) WHERE id % 10 = 0; +)); +$primary->safe_psql('postgres', 'CHECKPOINT'); +$primary->wait_for_catchup($standby); + +# The workload far exceeds the standby's shared_buffers, so replay must +# have evicted dirty pages through the standby's own DWB write path. +$standby->safe_psql('postgres', 'CHECKPOINT'); +is( $standby->safe_psql( + 'postgres', + "SELECT sum(writes) > 0 FROM pg_stat_io WHERE object = 'dwb'"), + 't', 'standby replay flushed pages through its own ring'); + +# The restartpoint moved the minRecoveryPoint contract forward: FlushBuffer +# on the standby cannot fsync WAL itself, it advances minRecoveryPoint +# through XLogFlush instead (see 3.9 of the design plan). +is( $standby->safe_psql( + 'postgres', + "SELECT min_recovery_end_lsn > '$mrp_before'::pg_lsn FROM pg_control_recovery()"), + 't', 'minRecoveryPoint advanced past the replayed flushes'); + +# and the ring keeps circulating: the worker drains it back to all-free +$standby->poll_query_until('postgres', + "SELECT test_dwb_states() LIKE 'free=16 %'") + or die 'timed out waiting for the standby ring to drain'; +pass('standby ring drained back to all-free'); + +# --- the standby survives its own crash ---------------------------------- + +$standby->stop('immediate'); +$standby_log_offset = -s $standby->logfile; +$standby->start; +ok( $standby->log_contains( + qr/double write buffer ring opened: 16 batches of 16 pages, generation 2/, + $standby_log_offset), + 'crashed standby reopened its ring under a bumped generation'); + +$primary->safe_psql('postgres', 'INSERT INTO dwb_t VALUES (100001, \'after standby crash\')'); +$primary->wait_for_catchup($standby); +is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), + '50001', 'replication resumed after the standby crash'); + +# --- the primary survives its own crash ---------------------------------- + +$primary->stop('immediate'); +$primary->start; +$primary->safe_psql('postgres', 'INSERT INTO dwb_t VALUES (100002, \'after primary crash\')'); +$primary->wait_for_catchup($standby); +is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), + '50002', 'replication resumed after the primary crash'); + +# --- promotion with a replay backlog ------------------------------------- + +# Pause replay, pile up a burst, make sure it is flushed to the standby's +# local WAL, then resume and promote: the promotion completes only after +# the backlog has replayed through the standby's DWB write path. +$standby->safe_psql('postgres', 'SELECT pg_wal_replay_pause()'); +$primary->safe_psql('postgres', q( + UPDATE dwb_t SET filler = repeat('p', 300) WHERE id % 3 = 0; + INSERT INTO dwb_t VALUES (100003, 'burst tail'); +)); +$primary->wait_for_catchup($standby, 'flush', $primary->lsn('write')); +$standby->safe_psql('postgres', 'SELECT pg_wal_replay_resume()'); +$standby->promote; + +is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), + '50003', 'promoted standby replayed the whole backlog'); +is( $standby->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + 'f', 'standby left recovery'); + +# the promoted node keeps writing through its ring as a primary +$standby->safe_psql('postgres', q( + UPDATE dwb_t SET filler = repeat('q', 300) WHERE id % 5 = 0; + INSERT INTO dwb_t VALUES (100004, 'after promotion'); +)); +$standby->safe_psql('postgres', 'CHECKPOINT'); +is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), + '50004', 'promoted node accepts writes through the DWB path'); +$standby->poll_query_until('postgres', + "SELECT test_dwb_states() LIKE 'free=16 %'") + or die 'timed out waiting for the promoted ring to drain'; +pass('promoted ring drained back to all-free'); + +done_testing(); diff --git a/src/test/modules/test_dwb/t/006_backup.pl b/src/test/modules/test_dwb/t/006_backup.pl new file mode 100644 index 0000000000000..46c1a5a49c1f5 --- /dev/null +++ b/src/test/modules/test_dwb/t/006_backup.pl @@ -0,0 +1,145 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Online backup under io_torn_pages_protection = double_writes: WAL carries +# no full-page images in normal running, but an active backup forces them +# back on (the ring only repairs local torn writes; a backup copied mid-write +# can hold a torn page that just WAL replay with FPIs must repair). The +# backup itself excludes pg_dwb/ contents, tolerates pg_dwb being a symlink, +# and a cluster restored from it cold-starts a fresh ring. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_backup'); +$node->init(allows_streaming => 1); +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 1 +dwb_batch_timeout_ms = 20 +autovacuum = off +)); +$node->start; + +$node->safe_psql('postgres', q( + CREATE TABLE dwb_fpi AS + SELECT g AS id, repeat('f', 64) AS pad FROM generate_series(1, 100) g; +)); + +# --- normal running writes no full-page images --------------------------- + +# The first touch of a page after a checkpoint is exactly where an FPI +# would go; under double_writes none may appear. +$node->safe_psql('postgres', 'CHECKPOINT'); +my $lsn0 = $node->safe_psql('postgres', 'SELECT pg_current_wal_insert_lsn()'); +$node->safe_psql('postgres', "UPDATE dwb_fpi SET pad = repeat('a', 64) WHERE id = 1"); +my $lsn1 = $node->safe_psql('postgres', 'SELECT pg_current_wal_insert_lsn()'); + +my ($waldump, $walerr) = run_command( + [ + 'pg_waldump', '--path' => $node->data_dir . '/pg_wal', + '--start' => $lsn0, '--end' => $lsn1 + ]); +like($waldump, qr/Heap/, 'the WAL window covers the update'); +unlike($waldump, qr/\bFPW\b/, 'no full-page image outside a backup'); + +# --- an active backup forces full-page images back on --------------------- + +# pg_backup_start checkpoints and raises runningBackups; the next touch of +# the same page must now carry an FPI (doPageWrites = +# Insert->fullPageWrites || runningBackups > 0). +my $bk = $node->background_psql('postgres'); +$bk->query_safe('SET client_min_messages = warning'); +$bk->query_safe("SELECT pg_backup_start('dwb_fpi_probe', true)"); + +my $lsn2 = $node->safe_psql('postgres', 'SELECT pg_current_wal_insert_lsn()'); +$node->safe_psql('postgres', "UPDATE dwb_fpi SET pad = repeat('b', 64) WHERE id = 2"); +my $lsn3 = $node->safe_psql('postgres', 'SELECT pg_current_wal_insert_lsn()'); + +($waldump, $walerr) = run_command( + [ + 'pg_waldump', '--path' => $node->data_dir . '/pg_wal', + '--start' => $lsn2, '--end' => $lsn3 + ]); +like($waldump, qr/\bFPW\b/, 'an active backup forces full-page images'); + +$bk->query_safe('SELECT pg_backup_stop()'); +$bk->quit; + +# --- the backup keeps pg_dwb as an empty directory ------------------------ + +my $backup_path = $node->backup_dir . '/content_check'; +my ($out, $err) = run_command( + [ + 'pg_basebackup', '--no-sync', + '--pgdata' => $backup_path, + '--host' => $node->host, + '--port' => $node->port, + '--checkpoint' => 'fast' + ]); +ok(-f "$backup_path/PG_VERSION", 'backup completed'); +unlike($err, qr/WARNING|skipping special file/, + 'pg_basebackup issued no warnings'); +ok(-d "$backup_path/pg_dwb", 'backup contains a pg_dwb directory'); +{ + opendir(my $dh, "$backup_path/pg_dwb") or die "opendir: $!"; + my @entries = grep { !/^\.\.?$/ } readdir($dh); + closedir($dh); + is(scalar(@entries), 0, 'the pg_dwb directory in the backup is empty'); +} + +# --- a cluster restored from the backup cold-starts its ring -------------- + +my $restored = PostgreSQL::Test::Cluster->new('dwb_restored'); +$restored->init_from_backup($node, 'content_check'); +my $restored_log_offset = -s $restored->logfile; +$restored->start; +ok( $restored->log_contains( + qr/double write buffer ring opened: 16 batches of 16 pages, generation 1/, + $restored_log_offset), + 'restored cluster opened a fresh ring'); +is( $restored->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpi'), + '100', 'restored data is intact'); +$restored->stop; + +# --- pg_dwb as a symlink backs up as an empty real directory -------------- + +SKIP: +{ + skip 'symlinks are not portable to Windows', 4 if $windows_os; + + $node->stop; + my $dwb_store = $node->basedir . '/dwb_store'; + rename($node->data_dir . '/pg_dwb', $dwb_store) + or die "rename pg_dwb: $!"; + symlink($dwb_store, $node->data_dir . '/pg_dwb') + or die "symlink pg_dwb: $!"; + $node->start; + + my $link_backup = $node->backup_dir . '/symlink_check'; + ($out, $err) = run_command( + [ + 'pg_basebackup', '--no-sync', + '--pgdata' => $link_backup, + '--host' => $node->host, + '--port' => $node->port, + '--checkpoint' => 'fast' + ]); + ok(-f "$link_backup/PG_VERSION", 'backup of the symlinked ring completed'); + unlike($err, qr/WARNING|skipping special file/, + 'no warnings for the symlinked pg_dwb'); + ok(-d "$link_backup/pg_dwb" && !-l "$link_backup/pg_dwb", + 'symlinked pg_dwb became a real directory in the backup'); + opendir(my $dh, "$link_backup/pg_dwb") or die "opendir: $!"; + my @entries = grep { !/^\.\.?$/ } readdir($dh); + closedir($dh); + is(scalar(@entries), 0, 'the symlinked pg_dwb backed up empty'); +} + +done_testing(); From d7a2ed13a1c756aa78124e9ce679c512fa9206ba Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 25 Jul 2026 07:18:44 +0300 Subject: [PATCH 10/52] Fix cross-class batch aliasing and harden the FPI-off surface (Stage 3 follow-up) The per-class open batch pointer could keep naming a freed batch index after the other class reopened it: open_batch_idx[] is only replaced when its batch is sealed, and reservations did not check who owns the current incarnation. A checkpointer flush could then join a live EVICTION batch and, on the no-pool path, seal it under the holder. next_slot_idx now carries a writer-class bit stamped at re-init under DWBRingOpenLock, reservations use a CAS that validates the seal and class bits atomically with the increment (a blind fetch_add into a foreign batch would leave a never-published slot and hang the leader's coverage wait), and DWBOpenNewBatch treats a foreign-class incarnation as needing replacement. The checkpoint scenario in 004 reproduces the aliasing deterministically and no longer needs its warmup choreography. The rest closes gaps around WAL that carries no full-page images: * pg_rewind now requires io_torn_pages_protection = "full_pages" (and full_page_writes = on) on a live source: pages read from a running server can be torn and only FPI replay repairs them, which double_writes WAL cannot provide. Rewinding from a stopped source stays available. New 007_rewind.pl also pins that the rewind wipes the target's own ring and the rewound node cold-starts a fresh one. * A mode change through a restart emits no XLOG_FPW_CHANGE (the end-of-recovery UpdateFullPageWrites call is gated on recovery being finished), so the standby backup guards never saw the transition and a backup taken before the next restartpoint silently spanned image-less WAL. Replayed checkpoint records declaring full_page_writes = off now advance lastFpwDisableRecPtr too, and the same helper warns once when a server replays image-less WAL without running a double write buffer of its own. * The do_pg_backup_start/stop refusals name io_torn_pages_protection and the workable paths instead of a full_page_writes hint that this mode ignores; 005 tests the refusal. Mode "off" logs its state at startup. pg_dwb is documented in protocol.sgml, backup.sgml and storage.sgml. Test hardening: 005 fits the runtime budget, asserts minRecoveryPoint advance from replay-driven flushes alone, verifies page contents, and promotes with the replay pause still in effect so the backlog is real; 006 checks pg_waldump errors and matches the forced FPI by file node; new 008_modes.pl pins mode "off", the legacy GUC under "full_pages" and the SIGHUP no-op under "double_writes". --- doc/src/sgml/backup.sgml | 6 +- doc/src/sgml/protocol.sgml | 3 +- doc/src/sgml/storage.sgml | 7 + src/backend/access/transam/xlog.c | 89 +++++++++--- src/backend/backup/basebackup.c | 8 +- src/backend/storage/dwb/dwb.c | 91 ++++++++---- src/backend/storage/dwb/dwb_recovery.c | 11 ++ src/bin/pg_rewind/filemap.c | 10 +- src/bin/pg_rewind/libpq_source.c | 19 ++- src/include/storage/dwb.h | 19 ++- src/test/modules/test_dwb/meson.build | 2 + .../modules/test_dwb/t/004_retire_paths.pl | 53 +++---- src/test/modules/test_dwb/t/005_standby.pl | 137 +++++++++++++----- src/test/modules/test_dwb/t/006_backup.pl | 40 +++-- src/test/modules/test_dwb/t/007_rewind.pl | 104 +++++++++++++ src/test/modules/test_dwb/t/008_modes.pl | 101 +++++++++++++ 16 files changed, 556 insertions(+), 144 deletions(-) create mode 100644 src/test/modules/test_dwb/t/007_rewind.pl create mode 100644 src/test/modules/test_dwb/t/008_modes.pl diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml index 25b8904baf7cd..850971a435a36 100644 --- a/doc/src/sgml/backup.sgml +++ b/doc/src/sgml/backup.sgml @@ -1130,11 +1130,15 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true); - The contents of the directories pg_dynshmem/, + The contents of the directories pg_dwb/, + pg_dynshmem/, pg_notify/, pg_serial/, pg_snapshots/, pg_stat_tmp/, and pg_subtrans/ (but not the directories themselves) can be omitted from the backup as they will be initialized on postmaster startup. + The double write buffer ring in pg_dwb/ in particular + must never be restored to another cluster: its page copies only repair + torn writes of the instance that wrote them. diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index f0b29ed8cab9f..0b62fe02afb29 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -3388,7 +3388,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" - pg_dynshmem, pg_notify, + pg_dwb, pg_dynshmem, + pg_notify, pg_replslot, pg_serial, pg_snapshots, pg_stat_tmp, and pg_subtrans are copied as empty directories (even if diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml index 61250799ec076..198b36a3e3344 100644 --- a/doc/src/sgml/storage.sgml +++ b/doc/src/sgml/storage.sgml @@ -77,6 +77,13 @@ Item Subdirectory containing transaction commit timestamp data + + pg_dwb + Subdirectory containing the double write buffer ring + (when io_torn_pages_protection is set to + double_writes) + + pg_dynshmem Subdirectory containing files used by the dynamic shared memory diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index fbab91461a553..b77351947f711 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -8213,14 +8213,16 @@ XLogReportParameters(void) } /* - * Whether WAL records should carry full-page images. + * The effective value of the full_page_writes setting. * * io_torn_pages_protection selects the torn-page protection mechanism: under * "double_writes" the durable copy in pg_dwb/ replaces FPIs and under "off" * the user has declared torn writes impossible, so both force this off; the * legacy full_page_writes GUC keeps its meaning under "full_pages" only. - * Online backups still force page images regardless of this value, through - * the runningBackups term of doPageWrites (see XLogInsertRecord). + * Online backups taken on a primary still force page images regardless of + * this value, through the runningBackups term of doPageWrites (see + * XLogInsertRecord); backups initiated on a standby cannot and are refused + * by do_pg_backup_start when the replayed WAL lacks page images. */ static bool EffectiveFullPageWrites(void) @@ -8296,6 +8298,42 @@ UpdateFullPageWrites(void) END_CRIT_SECTION(); } +/* + * Track the last replayed WAL record declaring full-page writes disabled, + * for the standby backup guards in do_pg_backup_start/stop. Both + * XLOG_FPW_CHANGE records and checkpoint records can carry the declaration: + * a primary restarted into a mode without page images + * (io_torn_pages_protection = "double_writes"/"off", or full_page_writes = + * off) emits no XLOG_FPW_CHANGE — its checkpoints are the only replayed + * evidence of the change (see UpdateFullPageWrites). + * + * Also the place to warn, once per startup process, when this server + * replays image-less WAL without running a double write buffer of its own: + * a crash would then leave torn data pages that nothing can repair. + */ +static void +XLogTrackFullPageWritesDisabled(XLogReaderState *record, bool fpw) +{ + static bool warned = false; + + if (fpw) + return; + + SpinLockAcquire(&XLogCtl->info_lck); + if (XLogCtl->lastFpwDisableRecPtr < record->ReadRecPtr) + XLogCtl->lastFpwDisableRecPtr = record->ReadRecPtr; + SpinLockRelease(&XLogCtl->info_lck); + + if (!warned && !DWBIsEnabled()) + { + ereport(WARNING, + (errmsg("replaying WAL generated without full page images, but this server does not use the double write buffer"), + errdetail("Torn data pages left by a crash cannot be repaired by this WAL or by this server's configuration."), + errhint("Set \"io_torn_pages_protection\" to \"double_writes\" on this server, or to \"full_pages\" on the server that generated the WAL."))); + warned = true; + } +} + /* * XLOG resource manager's routines * @@ -8341,6 +8379,7 @@ xlog_redo(XLogReaderState *record) TimeLineID replayTLI; memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint)); + XLogTrackFullPageWritesDisabled(record, checkPoint.fullPageWrites); /* In a SHUTDOWN checkpoint, believe the counters exactly */ LWLockAcquire(XidGenLock, LW_EXCLUSIVE); TransamVariables->nextXid = checkPoint.nextXid; @@ -8447,6 +8486,7 @@ xlog_redo(XLogReaderState *record) TimeLineID replayTLI; memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint)); + XLogTrackFullPageWritesDisabled(record, checkPoint.fullPageWrites); /* In an ONLINE checkpoint, treat the XID counter as a minimum */ LWLockAcquire(XidGenLock, LW_EXCLUSIVE); if (FullTransactionIdPrecedes(TransamVariables->nextXid, @@ -8654,17 +8694,11 @@ xlog_redo(XLogReaderState *record) memcpy(&fpw, XLogRecGetData(record), sizeof(bool)); /* - * Update the LSN of the last replayed XLOG_FPW_CHANGE record so that - * do_pg_backup_start() and do_pg_backup_stop() can check whether - * full_page_writes has been disabled during online backup. + * Track the disable point so that do_pg_backup_start() and + * do_pg_backup_stop() can check whether full-page writes were + * disabled during an online backup. */ - if (!fpw) - { - SpinLockAcquire(&XLogCtl->info_lck); - if (XLogCtl->lastFpwDisableRecPtr < record->ReadRecPtr) - XLogCtl->lastFpwDisableRecPtr = record->ReadRecPtr; - SpinLockRelease(&XLogCtl->info_lck); - } + XLogTrackFullPageWritesDisabled(record, fpw); /* Keep track of full_page_writes */ lastFullPageWrites = fpw; @@ -9010,12 +9044,17 @@ do_pg_backup_start(const char *backupidstr, bool fast, List **tablespaces, if (!checkpointfpw || state->startpoint <= recptr) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("WAL generated with \"full_page_writes=off\" was replayed " + errmsg("WAL generated without full page images was replayed " "since last restartpoint"), - errhint("This means that the backup being taken on the standby " - "is corrupt and should not be used. " - "Enable \"full_page_writes\" and run CHECKPOINT on the primary, " - "and then try an online backup again."))); + errdetail("The primary does not write full page images: it runs " + "io_torn_pages_protection = \"double_writes\" or \"off\", " + "or full_page_writes is disabled."), + errhint("A backup taken on a standby needs full page images in the " + "replayed WAL; the primary's double write buffer cannot " + "substitute for them. Set io_torn_pages_protection = " + "\"full_pages\" (with \"full_page_writes\" enabled) on the " + "primary and run CHECKPOINT there, or take the backup on " + "the primary."))); /* * During recovery, since we don't use the end-of-backup WAL @@ -9306,12 +9345,16 @@ do_pg_backup_stop(BackupState *state, bool waitforarchive) if (state->startpoint <= recptr) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("WAL generated with \"full_page_writes=off\" was replayed " + errmsg("WAL generated without full page images was replayed " "during online backup"), - errhint("This means that the backup being taken on the standby " - "is corrupt and should not be used. " - "Enable \"full_page_writes\" and run CHECKPOINT on the primary, " - "and then try an online backup again."))); + errdetail("The backup being taken on the standby is corrupt " + "and should not be used."), + errhint("A backup taken on a standby needs full page images in the " + "replayed WAL; the primary's double write buffer cannot " + "substitute for them. Set io_torn_pages_protection = " + "\"full_pages\" (with \"full_page_writes\" enabled) on the " + "primary and run CHECKPOINT there, or take the backup on " + "the primary."))); LWLockAcquire(ControlFileLock, LW_SHARED); diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c index cd28fee3ea3e1..16fddfd855c2f 100644 --- a/src/backend/backup/basebackup.c +++ b/src/backend/backup/basebackup.c @@ -168,10 +168,10 @@ static const char *const excludeDirContents[] = PG_DYNSHMEM_DIR, /* - * The double write buffer ring only repairs torn writes on the local - * instance; restoring it elsewhere would let the apply-pass overwrite - * pages with copies from the backup moment. A restored cluster starts - * with a fresh ring instead, see DWBStartup(). + * The double write buffer ring holds page copies belonging to the + * instance being backed up; they are meaningless anywhere else and must + * never be applied to a restored cluster. A restored cluster cold-starts + * a fresh ring instead, see DWBStartup(). */ DWB_DIR, diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 95dd901d841e0..15d8ad96812f1 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -5,13 +5,14 @@ * * Batch lifecycle: FREE -> ALLOCATED -> SEALED -> WRITTEN -> FSYNCED -> * DATA_WRITTEN -> RETIRING -> FREE. Writers reserve slots with an atomic - * fetch_add on next_slot_idx (31-bit index + SEAL_BIT sentinel), publish - * their page image with a plain memcpy into the batch's staging buffer and - * set their bit in slots_written_bitmap. The SEAL initiator becomes the - * leader: it waits for bitmap coverage of capped_slots, then writes the - * whole batch — in this write order: the contiguous image stream, then the - * meta region, then fdatasync (the on-disk layout puts the meta region - * first; see dwb.h) — and broadcasts DWB_FSYNCED. + * CAS on next_slot_idx (30-bit index + writer-class bit + SEAL_BIT + * sentinel; see dwb.h), publish their page image with a plain memcpy into + * the batch's staging buffer and set their bit in slots_written_bitmap. + * The SEAL initiator becomes the leader: it waits for bitmap coverage of + * capped_slots, then writes the whole batch — in this write order: the + * contiguous image stream, then the meta region, then fdatasync (the + * on-disk layout puts the meta region first; see dwb.h) — and broadcasts + * DWB_FSYNCED. * * FlushBuffer drives this through DWBStagePageWrite/DWBFinishPageWrite; * retirement (segment fsyncs, the worker pool) lives in dwb_retire.c. @@ -261,14 +262,16 @@ DWBStagingRelease(int idx) /* * Make open_batch_idx[wclass] point at an ALLOCATED batch, if it currently - * points at old_idx (a sealed or invalid batch). Serialized by - * DWBRingOpenLock; sleeps on cv_free_batch when the whole ring is busy. + * points at old_idx (a sealed, foreign-class or invalid batch). Serialized + * by DWBRingOpenLock; sleeps on cv_free_batch when the whole ring is busy. * * Ordering note for stale writers: a batch keeps SEAL_BIT in next_slot_idx - * from its SEAL until we finish re-initializing it here, so a stale - * fetch_add against a reused batch either sees SEAL_BIT (and retries) or - * lands on a valid slot of the new incarnation — never on a slot that a - * concurrent reset can wipe. + * from its SEAL until we finish re-initializing it here, and the + * re-initialization stamps the opening class into DWB_WCLASS_BIT, so a + * stale reservation attempt against a reused batch either sees SEAL_BIT or + * a foreign class bit (and retries) or lands on a valid slot of a new + * same-class incarnation — never on a slot that a concurrent reset can + * wipe, and never in a batch the other class is filling. * * Non-static only for test_dwb's stale-open regression test. */ @@ -300,24 +303,38 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) * a slow opener gets here, old_idx may name a NEW live incarnation of * the same slot (sealed, retired, freed and reopened behind our * back), and replacing it would orphan that live batch together with - * its staging buffer. SEAL_BIT disambiguates the incarnations: it is - * set from SEAL through FREE and cleared only by the - * re-initialization below, under this same lock — so the open batch - * needs replacing if and only if its SEAL_BIT is set. + * its staging buffer. SEAL_BIT plus the class bit disambiguate the + * incarnations: SEAL_BIT is set from SEAL through FREE and cleared + * only by the re-initialization below (under this same lock), which + * also stamps the opening class — so the open batch needs replacing + * if and only if it is sealed or belongs to the other class (a reused + * index that the other class reopened while our pointer kept naming + * it). */ { uint32 cur = pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]); - if (cur != old_idx || - (cur != DWB_INVALID_BATCH && - !(pg_atomic_read_u32(&DWBCtl->batches[cur].next_slot_idx) & - DWB_SEAL_BIT))) + if (cur != old_idx) { LWLockRelease(DWBRingOpenLock); DWBStagingRelease(staging_idx); ConditionVariableCancelSleep(); return; } + if (cur != DWB_INVALID_BATCH) + { + uint32 nsi = pg_atomic_read_u32(&DWBCtl->batches[cur].next_slot_idx); + + if (!(nsi & DWB_SEAL_BIT) && + (nsi & DWB_WCLASS_BIT) == DWBWClassBit(wclass)) + { + /* still our live open batch */ + LWLockRelease(DWBRingOpenLock); + DWBStagingRelease(staging_idx); + ConditionVariableCancelSleep(); + return; + } + } } /* @@ -365,10 +382,12 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) /* * Open for reservations only after everything above is visible: - * clearing SEAL_BIT is the point where writers may enter. + * clearing SEAL_BIT is the point where writers may enter. The + * write also stamps the opening class into DWB_WCLASS_BIT, which + * reservations validate atomically with their increment. */ pg_write_barrier(); - pg_atomic_write_u32(&batch->next_slot_idx, 0); + pg_atomic_write_u32(&batch->next_slot_idx, DWBWClassBit(wclass)); pg_atomic_write_u32(&DWBCtl->open_batch_idx[wclass], free_idx); LWLockRelease(DWBRingOpenLock); @@ -683,11 +702,31 @@ DWBAcquireSlot(const BufferTag *tag, int wclass, bool use_resowner, } batch = &DWBCtl->batches[idx]; - prev = pg_atomic_fetch_add_u32(&batch->next_slot_idx, 1); - if (prev & DWB_SEAL_BIT) + /* + * Reserve with a CAS rather than a plain fetch_add: the seal and + * class bits are validated atomically with the increment. The class + * check is what makes a stale open pointer safe: the ring reuses + * indexes, so idx may name a batch that was freed and reopened under + * the OTHER class while our per-class pointer kept naming it, and a + * blind increment there would consume a slot nobody ever publishes — + * the leader would wait for its coverage forever. + */ + prev = pg_atomic_read_u32(&batch->next_slot_idx); + for (;;) + { + if (prev & DWB_SEAL_BIT) + break; /* sealed: reopen and retry */ + if ((prev & DWB_WCLASS_BIT) != DWBWClassBit(wclass)) + break; /* foreign incarnation: our pointer is stale */ + if (pg_atomic_compare_exchange_u32(&batch->next_slot_idx, + &prev, prev + 1)) + break; /* reserved */ + } + + if ((prev & DWB_SEAL_BIT) || + (prev & DWB_WCLASS_BIT) != DWBWClassBit(wclass)) { - /* already sealed; the extra increment is harmless (3.4) */ DWBOpenNewBatch(wclass, idx); continue; } diff --git a/src/backend/storage/dwb/dwb_recovery.c b/src/backend/storage/dwb/dwb_recovery.c index 76fe20f5b60fc..124c6ab6bc03c 100644 --- a/src/backend/storage/dwb/dwb_recovery.c +++ b/src/backend/storage/dwb/dwb_recovery.c @@ -33,7 +33,18 @@ DWBStartup(void) DWBControlFileData control; if (!DWBIsEnabled()) + { + /* + * The most dangerous mode must not be the quietest one: with "off" + * neither page images nor the ring protect data files, and the legacy + * full_page_writes GUC may still read "on". + */ + if (io_torn_pages_protection == DWB_PROTECT_OFF) + ereport(LOG, + (errmsg("torn page protection is disabled (io_torn_pages_protection = \"off\")"), + errdetail("WAL carries no full page images; \"full_page_writes\" is ignored in this mode."))); return; + } /* 3.1.7: a torn page with an intact header must never pass unnoticed */ if (!DataChecksumsEnabled()) diff --git a/src/bin/pg_rewind/filemap.c b/src/bin/pg_rewind/filemap.c index 812fc8a115747..73fd31c53b044 100644 --- a/src/bin/pg_rewind/filemap.c +++ b/src/bin/pg_rewind/filemap.c @@ -133,10 +133,12 @@ static const char *const excludeDirContents[] = "pg_dynshmem", /* defined as PG_DYNSHMEM_DIR */ /* - * The double write buffer ring is local to an instance; copying the - * source's ring over would hand the target foreign page copies. The - * target's own leftover ring is inert: the durable generation bump on - * every start (see DWBStartup()) keeps its slots out of any apply-pass. + * The double write buffer ring is local to an instance: its slots are + * page copies of that cluster's own in-flight writes. Excluding it keeps + * the source's ring off the target and, because decide_file_action() + * removes excluded paths that exist in the target, also wipes the + * target's own ring — the rewound cluster cold-starts a fresh one, see + * DWBStartup(). */ "pg_dwb", /* defined as DWB_DIR */ diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c index 56c2ad55d4a67..2ca1bcfde7f57 100644 --- a/src/bin/pg_rewind/libpq_source.c +++ b/src/bin/pg_rewind/libpq_source.c @@ -132,10 +132,23 @@ init_libpq_conn(PGconn *conn) PQclear(res); /* - * Also check that full_page_writes is enabled. We can get torn pages if - * a page is modified while we read it with pg_read_binary_file(), and we - * rely on full page images to fix them. + * Also check that the source server actually writes full page images. We + * can get torn pages if a page is modified while we read it with + * pg_read_binary_file(), and we rely on full page images to fix them. The + * full_page_writes GUC alone is not the authority: under + * io_torn_pages_protection = "double_writes" or "off" page images are + * forced off while the GUC may still read "on" (its value only matters + * under "full_pages"). The double write buffer cannot substitute here: + * it repairs torn writes of its own instance, not torn reads of a remote + * copy. Rewinding from a stopped source (--source-pgdata) has no such + * requirement. */ + str = run_simple_query(conn, "SHOW io_torn_pages_protection"); + if (strcmp(str, "full_pages") != 0) + pg_fatal("\"io_torn_pages_protection\" must be \"full_pages\" in the source server, not \"%s\"", + str); + pg_free(str); + str = run_simple_query(conn, "SHOW full_page_writes"); if (strcmp(str, "on") != 0) pg_fatal("\"full_page_writes\" must be enabled in the source server"); diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 9f76ec477efc8..da9f6a38b4ba2 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -235,10 +235,25 @@ typedef struct DWSegEntry #define DWB_EVICT_RESERVE Max(2, dwb_num_batches / 8) /* - * next_slot_idx encoding: 31-bit index + seal sentinel bit. + * next_slot_idx encoding: 30-bit index + writer-class bit + seal sentinel. + * + * The class bit records which writer class opened this incarnation of the + * batch. A reservation validates it atomically with the increment (CAS in + * DWBAcquireSlot), so a stale per-class open pointer can never join a batch + * that was freed and reopened under the other class: the ring reuses batch + * indexes, and open_batch_idx[] of an idle class keeps naming its last batch + * long after that batch was retired. */ #define DWB_SEAL_BIT (1U << 31) -#define DWB_IDX_MASK (DWB_SEAL_BIT - 1) +#define DWB_WCLASS_BIT (1U << 30) +#define DWB_IDX_MASK (DWB_WCLASS_BIT - 1) + +/* one flag bit encodes the opening class: works for exactly two classes */ +StaticAssertDecl(DWB_NUM_WCLASSES == 2, + "next_slot_idx has a single writer-class bit"); + +#define DWBWClassBit(wclass) \ + ((wclass) == DWB_WCLASS_BACKGROUND ? DWB_WCLASS_BIT : 0) typedef struct DWBatchCtl { diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index dcf1736fe023c..f9f8bffe7fc92 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -43,6 +43,8 @@ tests += { 't/004_retire_paths.pl', 't/005_standby.pl', 't/006_backup.pl', + 't/007_rewind.pl', + 't/008_modes.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/004_retire_paths.pl b/src/test/modules/test_dwb/t/004_retire_paths.pl index 48149d35305ee..0442090abba2e 100644 --- a/src/test/modules/test_dwb/t/004_retire_paths.pl +++ b/src/test/modules/test_dwb/t/004_retire_paths.pl @@ -59,7 +59,7 @@ $node->safe_psql('postgres', "SELECT test_dwb_checkpoint_pending($filenode)"); like( $node->safe_psql('postgres', 'SELECT test_dwb_states()'), - qr/retiring=1/, 'one batch parked in RETIRING with a pending sync request'); + qr/retiring=1$/, 'one batch parked in RETIRING with a pending sync request'); $node->safe_psql('postgres', 'CHECKPOINT'); like( $node->safe_psql('postgres', 'SELECT test_dwb_states()'), @@ -67,38 +67,27 @@ # --- a checkpoint tolerates a live ALLOCATED batch ------------------------ -# Checkpoints take no DWB barrier: an open batch whose timeout has not -# fired stays ALLOCATED across a CHECKPOINT and is finished asynchronously. +# Checkpoints take no DWB barrier. With no retire workers nothing seals +# behind our back, so the holder's open batch must stay ALLOCATED across a +# CHECKPOINT; the test seals and retires it explicitly once the holder is +# gone. # -# Warmup: a throwaway session runs the same statements once and a -# CHECKPOINT flushes every catalog page its login dirtied (hint bits under -# checksums), so the real holder below leaves no dirty buffer for the -# checkpoint to feed through the DWB write path — which would seal the -# open batch as a side effect. -my $warm = $node->background_psql('postgres'); -$warm->query_safe('SELECT test_dwb_leak(1, true)'); -$warm->quit; -$node->poll_query_until('postgres', - "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " - . "CASE WHEN test_dwb_retire() >= 0 THEN " - . "test_dwb_states() LIKE 'free=64 %' END END") - or die 'timed out draining the warmup batch'; -$node->safe_psql('postgres', 'CHECKPOINT'); -$node->poll_query_until('postgres', - "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " - . "CASE WHEN test_dwb_retire() >= 0 THEN " - . "test_dwb_states() LIKE 'free=64 %' END END") - or die 'timed out draining the warmup checkpoint traffic'; - +# This doubles as the regression test for cross-class open-pointer +# aliasing: the checkpoints above left open_batch_idx[BACKGROUND] naming a +# long-freed batch index, the holder's EVICTION-class open reuses exactly +# that index (lowest FREE), and the CHECKPOINT below makes the checkpointer +# flush the holder's login hint bits through the DWB. Without the writer +# class stamp in next_slot_idx the checkpointer would join the holder's +# batch and, on the no-pool path, seal it. my $holder = $node->background_psql('postgres'); $holder->query_safe('SELECT test_dwb_leak(1, true)'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), - qr/allocated=1/, 'an open ALLOCATED batch is live before the checkpoint'); +my $one_open = + 'free=63 allocated=1 sealed=0 written=0 fsynced=0 data_written=0 retiring=0'; +is( $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + $one_open, 'an open ALLOCATED batch is live before the checkpoint'); $node->safe_psql('postgres', 'CHECKPOINT'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), - qr/allocated=1/, 'CHECKPOINT completed and left the open batch alone'); +is( $node->safe_psql('postgres', 'SELECT test_dwb_states()'), + $one_open, 'CHECKPOINT completed and left the open batch alone'); $holder->quit; $node->poll_query_until('postgres', "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " @@ -140,11 +129,11 @@ $node->safe_psql('postgres', 'SELECT test_dwb_park(98000)'); like( $node->safe_psql('postgres', 'SELECT test_dwb_states()'), - qr/retiring=1/, 'batch parked for the stale-snapshot scenario'); + qr/retiring=1$/, 'batch parked for the stale-snapshot scenario'); $node->safe_psql('postgres', 'SELECT test_dwb_stale_snapshot(98000)'); like( $node->safe_psql('postgres', 'SELECT test_dwb_states()'), - qr/retiring=1/, 'stale snapshot dropped, parked batch still RETIRING'); + qr/retiring=1$/, 'stale snapshot dropped, parked batch still RETIRING'); $node->poll_query_until('postgres', "SELECT CASE WHEN test_dwb_retire() >= 0 THEN " . "test_dwb_states() LIKE 'free=64 %' END") @@ -174,7 +163,7 @@ 'soft fsync failure reported as a WARNING'); like( $node->safe_psql('postgres', 'SELECT test_dwb_states()'), - qr/retiring=1/, 'batch stays RETIRING after the soft fsync failure'); + qr/retiring=1$/, 'batch stays RETIRING after the soft fsync failure'); rmdir $segdir or die "rmdir $segdir: $!"; is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), diff --git a/src/test/modules/test_dwb/t/005_standby.pl b/src/test/modules/test_dwb/t/005_standby.pl index c7ce937602c86..b522905f68837 100644 --- a/src/test/modules/test_dwb/t/005_standby.pl +++ b/src/test/modules/test_dwb/t/005_standby.pl @@ -3,8 +3,9 @@ # Hot standby under io_torn_pages_protection = double_writes: the standby # runs its own ring while replaying, both sides survive crashes, promotion -# works with a replay backlog, and the minRecoveryPoint contract holds for -# pages the standby flushes through the DWB. +# drains a replay backlog through the ring, the minRecoveryPoint contract +# holds for replay-driven flushes, and a base backup initiated on the +# standby is refused loudly. use strict; use warnings FATAL => 'all'; @@ -14,8 +15,9 @@ my $primary = PostgreSQL::Test::Cluster->new('dwb_primary'); $primary->init(allows_streaming => 1); -# A tiny buffer pool forces replay evictions through the standby's ring; -# fsync must be ON so restartpoint ProcessSyncRequests retires batches. +# The workload table (~3.4 MB) exceeds shared_buffers, so replay on the +# standby must evict through its ring; fsync stays ON so the restartpoint +# ProcessSyncRequests path runs its DWB wrap for real. $primary->append_conf( 'postgresql.conf', qq( io_torn_pages_protection = double_writes @@ -35,13 +37,13 @@ $primary->backup('bkp'); my $standby = PostgreSQL::Test::Cluster->new('dwb_standby'); $standby->init_from_backup($primary, 'bkp', has_streaming => 1); -my $standby_log_offset = -s $standby->logfile; +my $standby_log_offset = (-s $standby->logfile) // 0; $standby->start; # pg_dwb/ is excluded from the backup, so the standby must open a fresh # ring rather than inherit the primary's. ok( $standby->log_contains( - qr/double write buffer ring opened: 16 batches of 16 pages, generation 1/, + qr/double write buffer ring opened: 16 batches of 16 pages, generation 1\b/, $standby_log_offset), 'standby cold-started a fresh ring from the base backup'); @@ -54,33 +56,47 @@ # --- replay traffic flows through the standby ring ----------------------- -my $mrp_before = $standby->safe_psql('postgres', - 'SELECT min_recovery_end_lsn FROM pg_control_recovery()'); - $primary->safe_psql('postgres', q( CREATE TABLE dwb_t AS SELECT g AS id, repeat('x', 300) AS filler - FROM generate_series(1, 50000) g; + FROM generate_series(1, 10000) g; UPDATE dwb_t SET filler = repeat('y', 300) WHERE id % 10 = 0; )); $primary->safe_psql('postgres', 'CHECKPOINT'); $primary->wait_for_catchup($standby); -# The workload far exceeds the standby's shared_buffers, so replay must -# have evicted dirty pages through the standby's own DWB write path. +# The workload exceeds the standby's shared_buffers, so the startup +# process itself must have evicted dirty pages through the standby's own +# DWB write path. Startup flushes its stats when it replays the +# XLOG_RUNNING_XACTS record the primary's CHECKPOINT above emitted, but +# that is asynchronous to wait_for_catchup — hence the poll. +$standby->poll_query_until('postgres', + "SELECT COALESCE(sum(writes), 0) > 0 FROM pg_stat_io " + . "WHERE object = 'dwb' AND backend_type = 'startup'") + or die 'timed out waiting for startup-process DWB writes on the standby'; +pass('replay evictions flowed through the standby ring'); + +is( $standby->safe_psql('postgres', + "SELECT count(*) FROM dwb_t WHERE filler = repeat('y', 300)"), + '1000', 'replayed page contents are correct'); + +# --- FlushBuffer on the standby advances minRecoveryPoint ---------------- + +# Take the baseline right after a restartpoint, then push replay-eviction +# traffic with NO further checkpoint or restartpoint anywhere: any advance +# past the baseline can then come only from buffer flushes — XLogFlush in +# recovery does not fsync WAL, it calls UpdateMinRecoveryPoint instead +# (see 3.9 of the design plan). $standby->safe_psql('postgres', 'CHECKPOINT'); -is( $standby->safe_psql( - 'postgres', - "SELECT sum(writes) > 0 FROM pg_stat_io WHERE object = 'dwb'"), - 't', 'standby replay flushed pages through its own ring'); - -# The restartpoint moved the minRecoveryPoint contract forward: FlushBuffer -# on the standby cannot fsync WAL itself, it advances minRecoveryPoint -# through XLogFlush instead (see 3.9 of the design plan). -is( $standby->safe_psql( - 'postgres', - "SELECT min_recovery_end_lsn > '$mrp_before'::pg_lsn FROM pg_control_recovery()"), - 't', 'minRecoveryPoint advanced past the replayed flushes'); +my $mrp_before = $standby->safe_psql('postgres', + 'SELECT min_recovery_end_lsn FROM pg_control_recovery()'); +$primary->safe_psql('postgres', + "UPDATE dwb_t SET filler = repeat('m', 300) WHERE id % 9 = 0"); +$primary->wait_for_catchup($standby); +$standby->poll_query_until('postgres', + "SELECT min_recovery_end_lsn > '$mrp_before'::pg_lsn FROM pg_control_recovery()") + or die 'minRecoveryPoint did not advance from replay-driven flushes alone'; +pass('replay-driven flushes advanced minRecoveryPoint without a restartpoint'); # and the ring keeps circulating: the worker drains it back to all-free $standby->poll_query_until('postgres', @@ -88,60 +104,111 @@ or die 'timed out waiting for the standby ring to drain'; pass('standby ring drained back to all-free'); +# --- a base backup initiated on the standby is refused loudly ------------ + +# The replayed WAL carries no page images and the primary's ring cannot +# substitute for them, so the vanilla do_pg_backup_start guard must refuse +# with a hint that names the real knob. +my $refused_path = $primary->backup_dir . '/standby_backup'; +my ($out, $err) = run_command( + [ + 'pg_basebackup', '--no-sync', + '--pgdata' => $refused_path, + '--host' => $standby->host, + '--port' => $standby->port, + '--checkpoint' => 'fast' + ]); +ok(!-f "$refused_path/PG_VERSION", + 'base backup from the standby is refused'); +like( + $err, + qr/WAL generated without full page images was replayed/, + '... loudly'); +like( + $err, + qr/io_torn_pages_protection/, + '... with a hint naming the real knob'); + # --- the standby survives its own crash ---------------------------------- $standby->stop('immediate'); $standby_log_offset = -s $standby->logfile; $standby->start; ok( $standby->log_contains( - qr/double write buffer ring opened: 16 batches of 16 pages, generation 2/, + qr/double write buffer ring opened: 16 batches of 16 pages, generation 2\b/, $standby_log_offset), 'crashed standby reopened its ring under a bumped generation'); -$primary->safe_psql('postgres', 'INSERT INTO dwb_t VALUES (100001, \'after standby crash\')'); +$primary->safe_psql('postgres', + "INSERT INTO dwb_t VALUES (100001, 'after standby crash')"); $primary->wait_for_catchup($standby); is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), - '50001', 'replication resumed after the standby crash'); + '10001', 'replication resumed after the standby crash'); # --- the primary survives its own crash ---------------------------------- $primary->stop('immediate'); $primary->start; -$primary->safe_psql('postgres', 'INSERT INTO dwb_t VALUES (100002, \'after primary crash\')'); +$primary->safe_psql('postgres', + "INSERT INTO dwb_t VALUES (100002, 'after primary crash')"); $primary->wait_for_catchup($standby); is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), - '50002', 'replication resumed after the primary crash'); + '10002', 'replication resumed after the primary crash'); # --- promotion with a replay backlog ------------------------------------- # Pause replay, pile up a burst, make sure it is flushed to the standby's -# local WAL, then resume and promote: the promotion completes only after -# the backlog has replayed through the standby's DWB write path. +# local WAL, and promote with the pause still in effect: promotion breaks +# the pause (recoveryPausesHere exits on the standby trigger), so the +# whole backlog demonstrably replays through the standby's DWB write path +# before the timeline switch. $standby->safe_psql('postgres', 'SELECT pg_wal_replay_pause()'); +$standby->poll_query_until('postgres', + "SELECT pg_get_wal_replay_pause_state() = 'paused'") + or die 'timed out waiting for replay to pause'; $primary->safe_psql('postgres', q( UPDATE dwb_t SET filler = repeat('p', 300) WHERE id % 3 = 0; INSERT INTO dwb_t VALUES (100003, 'burst tail'); )); $primary->wait_for_catchup($standby, 'flush', $primary->lsn('write')); -$standby->safe_psql('postgres', 'SELECT pg_wal_replay_resume()'); +is( $standby->safe_psql('postgres', + 'SELECT pg_last_wal_replay_lsn() < pg_last_wal_receive_lsn()'), + 't', 'a real replay backlog exists at promotion time'); $standby->promote; is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), - '50003', 'promoted standby replayed the whole backlog'); + '10003', 'promoted standby replayed the whole backlog'); +is( $standby->safe_psql('postgres', + "SELECT count(*) FROM dwb_t WHERE filler = repeat('p', 300)"), + '3334', 'backlog page contents are correct'); is( $standby->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), 'f', 'standby left recovery'); -# the promoted node keeps writing through its ring as a primary +# --- the promoted node is a full DWB primary ----------------------------- + +my $tl2_start = $standby->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); $standby->safe_psql('postgres', q( UPDATE dwb_t SET filler = repeat('q', 300) WHERE id % 5 = 0; INSERT INTO dwb_t VALUES (100004, 'after promotion'); )); +my $tl2_end = $standby->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); $standby->safe_psql('postgres', 'CHECKPOINT'); is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), - '50004', 'promoted node accepts writes through the DWB path'); + '10004', 'promoted node accepts writes'); $standby->poll_query_until('postgres', "SELECT test_dwb_states() LIKE 'free=16 %'") or die 'timed out waiting for the promoted ring to drain'; pass('promoted ring drained back to all-free'); +# the new timeline still carries no page images +my ($waldump, $walerr) = run_command( + [ + 'pg_waldump', '--path' => $standby->data_dir . '/pg_wal', + '--timeline' => 2, + '--start' => $tl2_start, '--end' => $tl2_end + ]); +is($walerr, '', 'pg_waldump read the post-promotion window cleanly'); +like($waldump, qr/Heap/, 'the window covers the post-promotion update'); +unlike($waldump, qr/\bFPW\b/, 'no full-page images after promotion'); + done_testing(); diff --git a/src/test/modules/test_dwb/t/006_backup.pl b/src/test/modules/test_dwb/t/006_backup.pl index 46c1a5a49c1f5..ca856374a4b13 100644 --- a/src/test/modules/test_dwb/t/006_backup.pl +++ b/src/test/modules/test_dwb/t/006_backup.pl @@ -3,10 +3,10 @@ # Online backup under io_torn_pages_protection = double_writes: WAL carries # no full-page images in normal running, but an active backup forces them -# back on (the ring only repairs local torn writes; a backup copied mid-write -# can hold a torn page that just WAL replay with FPIs must repair). The -# backup itself excludes pg_dwb/ contents, tolerates pg_dwb being a symlink, -# and a cluster restored from it cold-starts a fresh ring. +# back on (the ring only repairs local torn writes; a backup copied +# mid-write can hold a torn page that only WAL replay with FPIs can +# repair). The backup itself excludes pg_dwb/ contents, tolerates pg_dwb +# being a symlink, and a cluster restored from it cold-starts a fresh ring. use strict; use warnings FATAL => 'all'; @@ -31,49 +31,63 @@ CREATE TABLE dwb_fpi AS SELECT g AS id, repeat('f', 64) AS pad FROM generate_series(1, 100) g; )); +my $fpi_filenode = + $node->safe_psql('postgres', "SELECT pg_relation_filenode('dwb_fpi')"); # --- normal running writes no full-page images --------------------------- # The first touch of a page after a checkpoint is exactly where an FPI -# would go; under double_writes none may appear. +# would go; under double_writes none may appear. Hint-bit records +# (FPI_FOR_HINT) can still show up in the window, but without doPageWrites +# they carry no page image and thus no FPW block flag — the probe below +# stays meaningful. $node->safe_psql('postgres', 'CHECKPOINT'); -my $lsn0 = $node->safe_psql('postgres', 'SELECT pg_current_wal_insert_lsn()'); +my $lsn0 = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); $node->safe_psql('postgres', "UPDATE dwb_fpi SET pad = repeat('a', 64) WHERE id = 1"); -my $lsn1 = $node->safe_psql('postgres', 'SELECT pg_current_wal_insert_lsn()'); +my $lsn1 = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); my ($waldump, $walerr) = run_command( [ 'pg_waldump', '--path' => $node->data_dir . '/pg_wal', '--start' => $lsn0, '--end' => $lsn1 ]); +is($walerr, '', 'pg_waldump read the no-backup window cleanly'); like($waldump, qr/Heap/, 'the WAL window covers the update'); unlike($waldump, qr/\bFPW\b/, 'no full-page image outside a backup'); # --- an active backup forces full-page images back on --------------------- -# pg_backup_start checkpoints and raises runningBackups; the next touch of -# the same page must now carry an FPI (doPageWrites = +# pg_backup_start checkpoints and raises runningBackups; the first touch +# of any page after that must carry an FPI (doPageWrites = # Insert->fullPageWrites || runningBackups > 0). my $bk = $node->background_psql('postgres'); $bk->query_safe('SET client_min_messages = warning'); $bk->query_safe("SELECT pg_backup_start('dwb_fpi_probe', true)"); -my $lsn2 = $node->safe_psql('postgres', 'SELECT pg_current_wal_insert_lsn()'); +my $lsn2 = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); $node->safe_psql('postgres', "UPDATE dwb_fpi SET pad = repeat('b', 64) WHERE id = 2"); -my $lsn3 = $node->safe_psql('postgres', 'SELECT pg_current_wal_insert_lsn()'); +my $lsn3 = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); ($waldump, $walerr) = run_command( [ 'pg_waldump', '--path' => $node->data_dir . '/pg_wal', '--start' => $lsn2, '--end' => $lsn3 ]); -like($waldump, qr/\bFPW\b/, 'an active backup forces full-page images'); +is($walerr, '', 'pg_waldump read the backup window cleanly'); +like( + $waldump, + qr!rel \d+/\d+/$fpi_filenode .* FPW!, + 'an active backup forces a full-page image of the touched page'); $bk->query_safe('SELECT pg_backup_stop()'); $bk->quit; # --- the backup keeps pg_dwb as an empty directory ------------------------ +# guard against a vacuous emptiness assert: the source ring is non-empty +ok(-f $node->data_dir . '/pg_dwb/control', + 'the source cluster has ring files to exclude'); + my $backup_path = $node->backup_dir . '/content_check'; my ($out, $err) = run_command( [ @@ -101,7 +115,7 @@ my $restored_log_offset = -s $restored->logfile; $restored->start; ok( $restored->log_contains( - qr/double write buffer ring opened: 16 batches of 16 pages, generation 1/, + qr/double write buffer ring opened: 16 batches of 16 pages, generation 1\b/, $restored_log_offset), 'restored cluster opened a fresh ring'); is( $restored->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpi'), diff --git a/src/test/modules/test_dwb/t/007_rewind.pl b/src/test/modules/test_dwb/t/007_rewind.pl new file mode 100644 index 0000000000000..37793f7e3817a --- /dev/null +++ b/src/test/modules/test_dwb/t/007_rewind.pl @@ -0,0 +1,104 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# pg_rewind on a double_writes pair: a live source is refused (its WAL has +# no full-page images to repair pages read mid-write), a stopped source +# works, the target's own ring is wiped by the rewind, and the rewound +# node cold-starts a fresh ring and follows the promoted primary. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node_a = PostgreSQL::Test::Cluster->new('dwb_rewind_a'); +$node_a->init(allows_streaming => 1); +$node_a->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 1 +dwb_batch_timeout_ms = 20 +autovacuum = off +wal_keep_size = 64MB +)); +$node_a->start; +$node_a->safe_psql('postgres', q( + CREATE TABLE dwb_r AS SELECT g AS id FROM generate_series(1, 100) g; +)); + +$node_a->backup('bkp'); +my $node_b = PostgreSQL::Test::Cluster->new('dwb_rewind_b'); +$node_b->init_from_backup($node_a, 'bkp', has_streaming => 1); +$node_b->start; +$node_a->wait_for_catchup($node_b); + +# --- diverge the timelines ----------------------------------------------- + +$node_b->promote; +$node_b->safe_psql('postgres', "INSERT INTO dwb_r VALUES (100001)"); +# A keeps running as the old primary and diverges past the fork point +$node_a->safe_psql('postgres', "INSERT INTO dwb_r VALUES (200001)"); +$node_a->stop('fast'); + +# --- a live double_writes source is refused ------------------------------ + +command_fails_like( + [ + 'pg_rewind', + '--target-pgdata' => $node_a->data_dir, + '--source-server' => $node_b->connstr('postgres') + ], + qr/"io_torn_pages_protection" must be "full_pages" in the source server/, + 'pg_rewind refuses a live source that writes no full-page images'); + +# --- a stopped source works and wipes the target ring -------------------- + +# leave proof on the target that the rewind, not a later cold start, +# removed the ring files +ok(-f $node_a->data_dir . '/pg_dwb/control', + 'the target has ring files before the rewind'); + +$node_b->stop('fast'); +command_ok( + [ + 'pg_rewind', + '--target-pgdata' => $node_a->data_dir, + '--source-pgdata' => $node_b->data_dir + ], + 'pg_rewind from a stopped source succeeds'); + +ok(-d $node_a->data_dir . '/pg_dwb', 'the target still has a pg_dwb directory'); +{ + opendir(my $dh, $node_a->data_dir . '/pg_dwb') or die "opendir: $!"; + my @entries = grep { !/^\.\.?$/ } readdir($dh); + closedir($dh); + is(scalar(@entries), 0, 'the rewind wiped the target ring'); +} + +# --- the rewound node cold-starts a ring and follows the new primary ----- + +$node_b->start; +# the rewind copied the source's configuration; restore this node's port +$node_a->append_conf('postgresql.conf', 'port = ' . $node_a->port); +$node_a->enable_streaming($node_b); +my $a_log_offset = -s $node_a->logfile; +$node_a->start; +ok( $node_a->log_contains( + qr/double write buffer ring opened: 16 batches of 16 pages, generation 1\b/, + $a_log_offset), + 'rewound node cold-started a fresh ring'); + +$node_b->wait_for_catchup($node_a); +is( $node_a->safe_psql('postgres', 'SELECT count(*) FROM dwb_r'), + '101', 'rewound node converged on the new primary timeline'); +is( $node_a->safe_psql('postgres', + 'SELECT count(*) FROM dwb_r WHERE id = 200001'), + '0', 'the divergent row is gone'); +is( $node_a->safe_psql('postgres', + 'SELECT count(*) FROM dwb_r WHERE id = 100001'), + '1', "the new primary's row is present"); + +done_testing(); diff --git a/src/test/modules/test_dwb/t/008_modes.pl b/src/test/modules/test_dwb/t/008_modes.pl new file mode 100644 index 0000000000000..9fedf15e13677 --- /dev/null +++ b/src/test/modules/test_dwb/t/008_modes.pl @@ -0,0 +1,101 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# The io_torn_pages_protection modes that do not use the ring: "off" +# forces full-page images off no matter what the legacy GUC says (and is +# loud about it), "full_pages" defers to the legacy full_page_writes GUC, +# and under "double_writes" a SIGHUP of the legacy GUC is a no-op. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_modes'); +$node->init; + +# A tiny helper: pg_waldump over the WAL the given statement generated. +sub wal_window +{ + my ($stmt) = @_; + my $lsn0 = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); + $node->safe_psql('postgres', $stmt); + my $lsn1 = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); + my ($out, $err) = run_command( + [ + 'pg_waldump', '--path' => $node->data_dir . '/pg_wal', + '--start' => $lsn0, '--end' => $lsn1 + ]); + is($err, '', "pg_waldump read the window of: $stmt"); + return $out; +} + +# --- mode "off": no page images regardless of the legacy GUC ------------- + +$node->append_conf('postgresql.conf', 'io_torn_pages_protection = off'); +my $log_offset = (-s $node->logfile) // 0; +$node->start; +ok( $node->log_contains( + qr/torn page protection is disabled/, $log_offset), + 'mode "off" announces itself'); +ok(!-d $node->data_dir . '/pg_dwb', 'mode "off" creates no ring'); +is( $node->safe_psql('postgres', 'SHOW full_page_writes'), + 'on', 'the legacy GUC still reads on...'); + +$node->safe_psql('postgres', + 'CREATE TABLE dwb_m AS SELECT g AS id FROM generate_series(1, 100) g'); +$node->safe_psql('postgres', 'CHECKPOINT'); +my $dump = wal_window('UPDATE dwb_m SET id = id WHERE id = 1'); +like($dump, qr/Heap/, '...the window covers the update...'); +unlike($dump, qr/\bFPW\b/, '...but no page image is written'); + +# --- mode "full_pages": the legacy GUC keeps its vanilla meaning --------- + +$node->append_conf('postgresql.conf', qq( +io_torn_pages_protection = full_pages +full_page_writes = off +)); +$node->restart; +$node->safe_psql('postgres', 'CHECKPOINT'); +$dump = wal_window('UPDATE dwb_m SET id = id WHERE id = 2'); +like($dump, qr/Heap/, 'full_pages + legacy off: window covers the update'); +unlike($dump, qr/\bFPW\b/, 'full_pages + legacy off: no page image'); + +$node->append_conf('postgresql.conf', 'full_page_writes = on'); +$node->restart; +$node->safe_psql('postgres', 'CHECKPOINT'); +$dump = wal_window('UPDATE dwb_m SET id = id WHERE id = 3'); +like($dump, qr/\bFPW\b/, 'full_pages + legacy on: page image written'); + +# --- mode "double_writes": a SIGHUP of the legacy GUC is a no-op --------- + +$node->append_conf('postgresql.conf', qq( +io_torn_pages_protection = double_writes +full_page_writes = on +)); +$node->restart; +$node->safe_psql('postgres', 'CHECKPOINT'); +$dump = wal_window('UPDATE dwb_m SET id = id WHERE id = 4'); +unlike($dump, qr/\bFPW\b/, 'double_writes ignores the legacy on'); + +my $reload_lsn = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); +$node->append_conf('postgresql.conf', 'full_page_writes = off'); +$node->reload; +# a CHECKPOINT forces a checkpointer cycle, which processes the pending +# SIGHUP (and would emit XLOG_FPW_CHANGE if the reload were not a no-op) +$node->safe_psql('postgres', 'CHECKPOINT'); +$dump = wal_window('UPDATE dwb_m SET id = id WHERE id = 5'); +unlike($dump, qr/\bFPW\b/, 'reloading the legacy GUC changes nothing'); + +my $lsn_end = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); +my ($out, $err) = run_command( + [ + 'pg_waldump', '--path' => $node->data_dir . '/pg_wal', + '--start' => $reload_lsn, '--end' => $lsn_end + ]); +is($err, '', 'pg_waldump read the reload window cleanly'); +unlike($out, qr/FPW_CHANGE/, + 'the no-op reload emitted no XLOG_FPW_CHANGE record'); + +done_testing(); From 4c1ddee2bdd2d2d675096b39a0202f63c2a4d89a Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 25 Jul 2026 09:00:45 +0300 Subject: [PATCH 11/52] Harden the pg_dwb surface of pg_rewind (Stage 3 follow-up) pg_rewind now supports a ring living behind a symlinked pg_dwb: the target traversal follows the link so the exclusion-driven removal wipes the ring through it, the entry itself is never created, removed or type-checked (the server recreates it lazily), and sync_pgdata() syncs the linked directory like a symlinked pg_wal, making the wipe durable. The source's link is deliberately not followed: its ring is never used, so a broken link there must not fail the rewind. A new up-front checkTargetDwb() runs before the target is touched in any way -- in particular before the single-user recovery run -- and rejects garbage the file-list traversal cannot see: a non-directory entry, a symlink not pointing at an accessible directory, or anything but regular files inside the flat ring. A restart transition into double_writes emits no XLOG_FPW_CHANGE, so the new 009 test pins the replayed-checkpoint tracking end to end: an online backup opened on a standby before the transition must fail pg_backup_stop(), and a standby without a ring of its own warns exactly once per startup. 007 grew refusal scenarios for every garbage pg_dwb layout, asserted against a crashed target via pg_controldata to prove the refusal precedes recovery. --- src/bin/pg_rewind/file_ops.c | 27 +++- src/bin/pg_rewind/file_ops.h | 3 +- src/bin/pg_rewind/filemap.c | 17 ++ src/bin/pg_rewind/local_source.c | 3 +- src/bin/pg_rewind/pg_rewind.c | 80 ++++++++- src/common/file_utils.c | 51 ++++-- src/include/storage/dwb.h | 3 +- src/test/modules/test_dwb/meson.build | 1 + src/test/modules/test_dwb/t/007_rewind.pl | 152 +++++++++++++++++- .../modules/test_dwb/t/009_fpw_transition.pl | 77 +++++++++ src/test/modules/test_dwb/test_dwb.c | 5 +- 11 files changed, 391 insertions(+), 28 deletions(-) create mode 100644 src/test/modules/test_dwb/t/009_fpw_transition.pl diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c index 074ac41b026fc..b1360b75367cc 100644 --- a/src/bin/pg_rewind/file_ops.c +++ b/src/bin/pg_rewind/file_ops.c @@ -37,7 +37,8 @@ static void create_target_symlink(const char *path, const char *link); static void remove_target_symlink(const char *path); static void recurse_dir(const char *datadir, const char *parentpath, - process_file_callback_t callback); + process_file_callback_t callback, + bool follow_dwb_symlink); /* * Open a target file for writing. If 'trunc' is true and the file already @@ -380,11 +381,17 @@ slurpFile(const char *datadir, const char *path, size_t *filesize) /* * Traverse through all files in a data directory, calling 'callback' * for each file. + * + * 'follow_dwb_symlink' says whether to follow a symlinked pg_dwb: the + * target's ring must be enumerated so that the rewind wipes it, but the + * source's ring is never used, so a broken link there must not fail the + * traversal. */ void -traverse_datadir(const char *datadir, process_file_callback_t callback) +traverse_datadir(const char *datadir, process_file_callback_t callback, + bool follow_dwb_symlink) { - recurse_dir(datadir, NULL, callback); + recurse_dir(datadir, NULL, callback, follow_dwb_symlink); } /* @@ -395,7 +402,7 @@ traverse_datadir(const char *datadir, process_file_callback_t callback) */ static void recurse_dir(const char *datadir, const char *parentpath, - process_file_callback_t callback) + process_file_callback_t callback, bool follow_dwb_symlink) { DIR *xldir; struct dirent *xlde; @@ -452,7 +459,7 @@ recurse_dir(const char *datadir, const char *parentpath, { callback(path, FILE_TYPE_DIRECTORY, 0, NULL); /* recurse to handle subdirectories */ - recurse_dir(datadir, path, callback); + recurse_dir(datadir, path, callback, follow_dwb_symlink); } else if (S_ISLNK(fst.st_mode)) { @@ -473,11 +480,15 @@ recurse_dir(const char *datadir, const char *parentpath, /* * If it's a symlink within pg_tblspc, we need to recurse into it, * to process all the tablespaces. We also follow a symlink if - * it's for pg_wal. Symlinks elsewhere are ignored. + * it's for pg_wal, or — when requested — for pg_dwb so that + * the target's double write buffer ring is enumerated (and thus + * wiped) even when the ring lives behind a symlink. Symlinks + * elsewhere are ignored. */ if ((parentpath && strcmp(parentpath, PG_TBLSPC_DIR) == 0) || - strcmp(path, "pg_wal") == 0) - recurse_dir(datadir, path, callback); + strcmp(path, "pg_wal") == 0 || + (follow_dwb_symlink && strcmp(path, "pg_dwb") == 0)) + recurse_dir(datadir, path, callback, follow_dwb_symlink); } } diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h index ee0d01df1ae51..e6ad847d87c6c 100644 --- a/src/bin/pg_rewind/file_ops.h +++ b/src/bin/pg_rewind/file_ops.h @@ -24,6 +24,7 @@ extern void sync_target_dir(void); extern char *slurpFile(const char *datadir, const char *path, size_t *filesize); typedef void (*process_file_callback_t) (const char *path, file_type_t type, size_t size, const char *link_target); -extern void traverse_datadir(const char *datadir, process_file_callback_t callback); +extern void traverse_datadir(const char *datadir, process_file_callback_t callback, + bool follow_dwb_symlink); #endif /* FILE_OPS_H */ diff --git a/src/bin/pg_rewind/filemap.c b/src/bin/pg_rewind/filemap.c index 73fd31c53b044..a53bf86375619 100644 --- a/src/bin/pg_rewind/filemap.c +++ b/src/bin/pg_rewind/filemap.c @@ -718,6 +718,23 @@ decide_file_action(file_entry_t *entry) if (strcmp(path, XLOG_CONTROL_FILE) == 0) return FILE_ACTION_NONE; + /* + * Never touch the pg_dwb entry itself: either side may have it as a plain + * directory, as a symlink, or (before its first double_writes startup) + * not at all, and the server (re)creates it lazily, see DWBCreateRing(). + * Its contents match the exclusion filters and are removed from the + * target below. The target's entry and ring contents are validated up + * front by checkTargetDwb() before the traversal; on the source side only + * a regular file can show up here, and it never has a legitimate reason + * to exist. + */ + if (strcmp(path, "pg_dwb") == 0) + { + if (entry->source_exists && entry->source_type == FILE_TYPE_REGULAR) + pg_fatal("\"%s\" in source is not a directory or symbolic link", path); + return FILE_ACTION_NONE; + } + /* Skip macOS system files */ if (strstr(path, ".DS_Store") != NULL) return FILE_ACTION_NONE; diff --git a/src/bin/pg_rewind/local_source.c b/src/bin/pg_rewind/local_source.c index 5a6e805c15833..89420a5e7d13c 100644 --- a/src/bin/pg_rewind/local_source.c +++ b/src/bin/pg_rewind/local_source.c @@ -57,7 +57,8 @@ init_local_source(const char *datadir) static void local_traverse_files(rewind_source *source, process_file_callback_t callback) { - traverse_datadir(((local_source *) source)->datadir, callback); + /* the source's double write buffer ring is never used, don't enter it */ + traverse_datadir(((local_source *) source)->datadir, callback, false); } static char * diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c index 9d16c1e6b4757..723d6b598736c 100644 --- a/src/bin/pg_rewind/pg_rewind.c +++ b/src/bin/pg_rewind/pg_rewind.c @@ -10,6 +10,7 @@ #include "postgres_fe.h" #include +#include #include #include #include @@ -46,6 +47,7 @@ static void digestControlFile(ControlFileData *ControlFile, const char *content, size_t size); static void getRestoreCommand(const char *argv0); static void sanityChecks(void); +static void checkTargetDwb(void); static TimeLineHistoryEntry *getTimelineHistory(TimeLineID tli, bool is_source, int *nentries); static void findCommonAncestorTimeline(TimeLineHistoryEntry *a_history, @@ -318,6 +320,8 @@ main(int argc, char **argv) else source = init_local_source(datadir_source); + checkTargetDwb(); + /* * Check the status of the target instance. * @@ -476,7 +480,7 @@ main(int argc, char **argv) if (showprogress) pg_log_info("reading target file list"); - traverse_datadir(datadir_target, &process_target_file); + traverse_datadir(datadir_target, &process_target_file, true); /* * Read the target WAL from last checkpoint before the point of fork, to @@ -780,6 +784,80 @@ sanityChecks(void) pg_fatal("source data directory must be shut down cleanly"); } +/* + * Validate the target's pg_dwb entry before the target is touched in any + * way — in particular before the single-user recovery run and the + * no-rewind-required exit. + * + * The file-list traversal classifies only directories, symlinks and regular + * files, so garbage in place of pg_dwb or inside it (a FIFO, a socket...) + * would go unnoticed and survive the rewind, only to fail the next + * double_writes startup. The entry must be a directory or a symlink to an + * accessible directory — or absent, since the server creates the ring + * lazily — and, the ring being flat, nothing but regular files belongs + * inside; anything else is rejected here, which lets the traversal's + * exclusion-driven removal wipe the ring completely. + */ +static void +checkTargetDwb(void) +{ + char dwb_path[MAXPGPATH]; + struct stat st; + DIR *dir; + struct dirent *de; + + snprintf(dwb_path, sizeof(dwb_path), "%s/pg_dwb", datadir_target); + + if (lstat(dwb_path, &st) < 0) + { + if (errno == ENOENT) + return; + pg_fatal("could not stat file \"%s\": %m", dwb_path); + } + + if (!S_ISDIR(st.st_mode) && !S_ISLNK(st.st_mode)) + pg_fatal("\"%s\" in target is not a directory or symbolic link", + "pg_dwb"); + + if (S_ISLNK(st.st_mode)) + { + if (stat(dwb_path, &st) < 0) + { + if (errno == ENOENT || errno == ENOTDIR) + pg_fatal("\"%s\" in target is a symbolic link that does not point to a directory", + "pg_dwb"); + pg_fatal("could not stat file \"%s\": %m", dwb_path); + } + if (!S_ISDIR(st.st_mode)) + pg_fatal("\"%s\" in target is a symbolic link that does not point to a directory", + "pg_dwb"); + } + + dir = opendir(dwb_path); + if (dir == NULL) + pg_fatal("could not open directory \"%s\": %m", dwb_path); + + while (errno = 0, (de = readdir(dir)) != NULL) + { + char entry_path[MAXPGPATH * 2]; + + if (strcmp(de->d_name, ".") == 0 || + strcmp(de->d_name, "..") == 0) + continue; + + snprintf(entry_path, sizeof(entry_path), "%s/%s", dwb_path, de->d_name); + if (lstat(entry_path, &st) < 0) + pg_fatal("could not stat file \"%s\": %m", entry_path); + if (!S_ISREG(st.st_mode)) + pg_fatal("\"%s/%s\" in target is not a regular file", + "pg_dwb", de->d_name); + } + if (errno) + pg_fatal("could not read directory \"%s\": %m", dwb_path); + + (void) closedir(dir); +} + /* * Print a progress report based on the fetch_size and fetch_done variables. * diff --git a/src/common/file_utils.c b/src/common/file_utils.c index 7b62687a2aa75..af31d4b962609 100644 --- a/src/common/file_utils.c +++ b/src/common/file_utils.c @@ -86,9 +86,9 @@ do_syncfs(const char *path) * Synchronize PGDATA and all its contents. * * We sync regular files and directories wherever they are, but we follow - * symlinks only for pg_wal (or pg_xlog) and immediately under pg_tblspc. - * Other symlinks are presumed to point at files we're not responsible for - * syncing, and might not have privileges to write at all. + * symlinks only for pg_wal (or pg_xlog), pg_dwb and immediately under + * pg_tblspc. Other symlinks are presumed to point at files we're not + * responsible for syncing, and might not have privileges to write at all. * * serverVersion indicates the version of the server to be sync'd. * @@ -102,12 +102,15 @@ sync_pgdata(const char *pg_data, bool sync_data_files) { bool xlog_is_symlink; + bool dwb_is_symlink; char pg_wal[MAXPGPATH]; + char pg_dwb[MAXPGPATH]; char pg_tblspc[MAXPGPATH]; /* handle renaming of pg_xlog to pg_wal in post-10 clusters */ snprintf(pg_wal, MAXPGPATH, "%s/%s", pg_data, serverVersion < MINIMUM_VERSION_FOR_PG_WAL ? "pg_xlog" : "pg_wal"); + snprintf(pg_dwb, MAXPGPATH, "%s/%s", pg_data, "pg_dwb"); snprintf(pg_tblspc, MAXPGPATH, "%s/%s", pg_data, PG_TBLSPC_DIR); /* @@ -125,6 +128,25 @@ sync_pgdata(const char *pg_data, xlog_is_symlink = true; } + /* + * Likewise for the double write buffer ring. Unlike pg_wal, pg_dwb is + * created lazily at the first double_writes startup, so its absence is + * normal and not worth a complaint; any other lstat() failure is. + */ + dwb_is_symlink = false; + + { + struct stat st; + + if (lstat(pg_dwb, &st) < 0) + { + if (errno != ENOENT) + pg_log_error("could not stat file \"%s\": %m", pg_dwb); + } + else if (S_ISLNK(st.st_mode)) + dwb_is_symlink = true; + } + switch (sync_method) { case DATA_DIR_SYNC_METHOD_SYNCFS: @@ -141,8 +163,9 @@ sync_pgdata(const char *pg_data, * On Linux, we don't have to open every single file one by * one. We can use syncfs() to sync whole filesystems. We * only expect filesystem boundaries to exist where we - * tolerate symlinks, namely pg_wal and the tablespaces, so we - * call syncfs() for each of those directories. + * tolerate symlinks, namely pg_wal, pg_dwb and the + * tablespaces, so we call syncfs() for each of those + * directories. */ /* Sync the top level pgdata directory. */ @@ -181,6 +204,10 @@ sync_pgdata(const char *pg_data, /* If pg_wal is a symlink, process that too. */ if (xlog_is_symlink) do_syncfs(pg_wal); + + /* Likewise for a symlinked double write buffer ring. */ + if (dwb_is_symlink) + do_syncfs(pg_dwb); #endif /* HAVE_SYNCFS */ } break; @@ -200,6 +227,8 @@ sync_pgdata(const char *pg_data, walkdir(pg_data, pre_sync_fname, false, exclude_dir); if (xlog_is_symlink) walkdir(pg_wal, pre_sync_fname, false, NULL); + if (dwb_is_symlink) + walkdir(pg_dwb, pre_sync_fname, false, NULL); if (sync_data_files) walkdir(pg_tblspc, pre_sync_fname, true, NULL); #endif @@ -208,15 +237,17 @@ sync_pgdata(const char *pg_data, * Now we do the fsync()s in the same order. * * The main call ignores symlinks, so in addition to specially - * processing pg_wal if it's a symlink, pg_tblspc has to be - * visited separately with process_symlinks = true. Note that - * if there are any plain directories in pg_tblspc, they'll - * get fsync'd twice. That's not an expected case so we don't - * worry about optimizing it. + * processing pg_wal and pg_dwb if they are symlinks, + * pg_tblspc has to be visited separately with + * process_symlinks = true. Note that if there are any plain + * directories in pg_tblspc, they'll get fsync'd twice. That's + * not an expected case so we don't worry about optimizing it. */ walkdir(pg_data, fsync_fname, false, exclude_dir); if (xlog_is_symlink) walkdir(pg_wal, fsync_fname, false, NULL); + if (dwb_is_symlink) + walkdir(pg_dwb, fsync_fname, false, NULL); if (sync_data_files) walkdir(pg_tblspc, fsync_fname, true, NULL); diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index da9f6a38b4ba2..f8d906413dd9a 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -258,7 +258,8 @@ StaticAssertDecl(DWB_NUM_WCLASSES == 2, typedef struct DWBatchCtl { pg_atomic_uint32 state; /* DWBatchState */ - pg_atomic_uint32 next_slot_idx; /* fetch_add on ALLOCATED */ + pg_atomic_uint32 next_slot_idx; /* CAS-incremented while open, see + * DWBAcquireSlot() */ pg_atomic_uint32 capped_slots; /* fixed by SEAL; leader waits for exactly * this many bitmap bits */ pg_atomic_uint64 slots_written_bitmap[DWB_BITMAP_WORDS]; diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index f9f8bffe7fc92..6d77cc442c7b0 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -45,6 +45,7 @@ tests += { 't/006_backup.pl', 't/007_rewind.pl', 't/008_modes.pl', + 't/009_fpw_transition.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/007_rewind.pl b/src/test/modules/test_dwb/t/007_rewind.pl index 37793f7e3817a..da3662f3eabc9 100644 --- a/src/test/modules/test_dwb/t/007_rewind.pl +++ b/src/test/modules/test_dwb/t/007_rewind.pl @@ -3,8 +3,10 @@ # pg_rewind on a double_writes pair: a live source is refused (its WAL has # no full-page images to repair pages read mid-write), a stopped source -# works, the target's own ring is wiped by the rewind, and the rewound -# node cold-starts a fresh ring and follows the promoted primary. +# works, the target's own ring is wiped by the rewind — also when pg_dwb +# is a symlink — and the rewound node cold-starts a fresh ring and follows +# the promoted primary. A regular file in place of pg_dwb is refused, and +# a broken ring symlink on the source is ignored. use strict; use warnings FATAL => 'all'; @@ -39,9 +41,29 @@ $node_b->promote; $node_b->safe_psql('postgres', "INSERT INTO dwb_r VALUES (100001)"); -# A keeps running as the old primary and diverges past the fork point +# A keeps running as the old primary and diverges past the fork point. +# Crash it: the refusal scenarios below must fire before pg_rewind's +# single-user recovery of the target gets a chance to run. $node_a->safe_psql('postgres', "INSERT INTO dwb_r VALUES (200001)"); -$node_a->stop('fast'); +$node_a->stop('immediate'); + +# --- relocate the target ring behind a symlink --------------------------- + +# Like pg_wal, pg_dwb may be a symlink to a directory on other storage; +# the rewind must wipe the ring through the link and leave the link +# itself in place. Creating symlinks requires a privilege on Windows, +# so the plain-directory layout is exercised there instead. +my $dwb_is_symlinked = 0; +unless ($windows_os) +{ + my $ring_home = PostgreSQL::Test::Utils::tempdir('dwb_ring'); + my $dwb_path = $node_a->data_dir . '/pg_dwb'; + rename($dwb_path, "$ring_home/pg_dwb") + or BAIL_OUT("could not move $dwb_path: $!"); + symlink("$ring_home/pg_dwb", $dwb_path) + or BAIL_OUT("could not symlink $dwb_path: $!"); + $dwb_is_symlinked = 1; +} # --- a live double_writes source is refused ------------------------------ @@ -62,6 +84,120 @@ 'the target has ring files before the rewind'); $node_b->stop('fast'); + +# --- a garbage entry in place of pg_dwb is refused ----------------------- + +# Anything but a directory, a symlink or nothing at all would survive the +# rewind only to fail the next double_writes startup, so the rewind must +# reject it before touching the target. +{ + my $dwb_path = $node_a->data_dir . '/pg_dwb'; + my $stash = $node_a->basedir . '/pg_dwb_stash'; + rename($dwb_path, $stash) or BAIL_OUT("could not move $dwb_path: $!"); + append_to_file($dwb_path, "not a ring\n"); + command_fails_like( + [ + 'pg_rewind', + '--target-pgdata' => $node_a->data_dir, + '--source-pgdata' => $node_b->data_dir + ], + qr/"pg_dwb" in target is not a directory or symbolic link/, + 'pg_rewind refuses a regular file in place of pg_dwb'); + unlink($dwb_path) or BAIL_OUT("could not remove $dwb_path: $!"); + + unless ($windows_os) + { + require POSIX; + + # a FIFO never even reaches the file map, so it takes the up-front + # check + POSIX::mkfifo($dwb_path, 0700) + or BAIL_OUT("could not create FIFO $dwb_path: $!"); + command_fails_like( + [ + 'pg_rewind', + '--target-pgdata' => $node_a->data_dir, + '--source-pgdata' => $node_b->data_dir + ], + qr/"pg_dwb" in target is not a directory or symbolic link/, + 'pg_rewind refuses a FIFO in place of pg_dwb'); + unlink($dwb_path) or BAIL_OUT("could not remove $dwb_path: $!"); + + # a symlink is only as good as what it points to + symlink('/nonexistent/dwb_ring_target', $dwb_path) + or BAIL_OUT("could not symlink $dwb_path: $!"); + command_fails_like( + [ + 'pg_rewind', + '--target-pgdata' => $node_a->data_dir, + '--source-pgdata' => $node_b->data_dir + ], + qr/"pg_dwb" in target is a symbolic link that does not point to a directory/, + 'pg_rewind refuses a broken pg_dwb symlink'); + unlink($dwb_path) or BAIL_OUT("could not remove $dwb_path: $!"); + + append_to_file("$stash.file", "not a ring\n"); + symlink("$stash.file", $dwb_path) + or BAIL_OUT("could not symlink $dwb_path: $!"); + command_fails_like( + [ + 'pg_rewind', + '--target-pgdata' => $node_a->data_dir, + '--source-pgdata' => $node_b->data_dir + ], + qr/"pg_dwb" in target is a symbolic link that does not point to a directory/, + 'pg_rewind refuses a pg_dwb symlink to a regular file'); + unlink($dwb_path) or BAIL_OUT("could not remove $dwb_path: $!"); + unlink("$stash.file") or BAIL_OUT("could not remove $stash.file: $!"); + + # garbage inside the ring is refused too: it would silently survive + # the wipe + mkdir($dwb_path) or BAIL_OUT("could not create $dwb_path: $!"); + POSIX::mkfifo("$dwb_path/control", 0700) + or BAIL_OUT("could not create FIFO $dwb_path/control: $!"); + command_fails_like( + [ + 'pg_rewind', + '--target-pgdata' => $node_a->data_dir, + '--source-pgdata' => $node_b->data_dir + ], + qr!"pg_dwb/control" in target is not a regular file!, + 'pg_rewind refuses a FIFO inside pg_dwb'); + unlink("$dwb_path/control") + or BAIL_OUT("could not remove $dwb_path/control: $!"); + rmdir($dwb_path) or BAIL_OUT("could not remove $dwb_path: $!"); + } + + # the crashed target was left alone throughout: every refusal came + # before the single-user recovery run + command_like( + [ 'pg_controldata', $node_a->data_dir ], + qr/Database cluster state:\s+in production/, + 'the refusals precede the single-user recovery of the target'); + + rename($stash, $dwb_path) or BAIL_OUT("could not restore $dwb_path: $!"); +} + +# return the target to a clean shutdown for the rewind proper; recovery +# runs with the ring already behind the symlink +$node_a->start; +$node_a->stop('fast'); + +# --- a broken ring symlink on the source is harmless --------------------- + +# The source's ring is never used, so only the target's pg_dwb link may be +# entered. Point the source's at nowhere and let the rewind below prove +# it: if the traversal followed the link, it would fail outright. +my $b_dwb_stash; +unless ($windows_os) +{ + my $b_dwb = $node_b->data_dir . '/pg_dwb'; + $b_dwb_stash = $node_b->basedir . '/pg_dwb_stash'; + rename($b_dwb, $b_dwb_stash) or BAIL_OUT("could not move $b_dwb: $!"); + symlink('/nonexistent/dwb_ring', $b_dwb) + or BAIL_OUT("could not symlink $b_dwb: $!"); +} + command_ok( [ 'pg_rewind', @@ -77,9 +213,17 @@ closedir($dh); is(scalar(@entries), 0, 'the rewind wiped the target ring'); } +ok(-l $node_a->data_dir . '/pg_dwb', 'the pg_dwb symlink survived the rewind') + if $dwb_is_symlinked; # --- the rewound node cold-starts a ring and follows the new primary ----- +if (defined $b_dwb_stash) +{ + my $b_dwb = $node_b->data_dir . '/pg_dwb'; + unlink($b_dwb) or BAIL_OUT("could not remove $b_dwb: $!"); + rename($b_dwb_stash, $b_dwb) or BAIL_OUT("could not restore $b_dwb: $!"); +} $node_b->start; # the rewind copied the source's configuration; restore this node's port $node_a->append_conf('postgresql.conf', 'port = ' . $node_a->port); diff --git a/src/test/modules/test_dwb/t/009_fpw_transition.pl b/src/test/modules/test_dwb/t/009_fpw_transition.pl new file mode 100644 index 0000000000000..78befa7eaaf96 --- /dev/null +++ b/src/test/modules/test_dwb/t/009_fpw_transition.pl @@ -0,0 +1,77 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# A primary restarted into io_torn_pages_protection = double_writes emits +# no XLOG_FPW_CHANGE (UpdateFullPageWrites at startup runs before recovery +# is marked done, and the checkpointer's later call sees no remaining +# change), so the checkpoints written after the restart are the only +# replayed evidence that page images stopped. Crossing that transition +# must fail pg_backup_stop() for an online backup opened on a standby, +# and a standby without a double write buffer of its own must warn once +# per startup. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $primary = PostgreSQL::Test::Cluster->new('dwb_fpw_primary'); +$primary->init(allows_streaming => 1); +$primary->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = full_pages +autovacuum = off +)); +$primary->start; + +$primary->backup('bkp'); +my $standby = PostgreSQL::Test::Cluster->new('dwb_fpw_standby'); +$standby->init_from_backup($primary, 'bkp', has_streaming => 1); +$standby->start; + +$primary->safe_psql('postgres', + 'CREATE TABLE dwb_fpw AS SELECT g AS id FROM generate_series(1, 1000) g'); +$primary->wait_for_catchup($standby); + +# --- an online backup opened on the standby while page images flow ------- + +# on_error_stop off: the session must survive the expected pg_backup_stop() +# error below +my $backer = $standby->background_psql('postgres', on_error_stop => 0); +$backer->query_safe("SELECT pg_backup_start('dwb_fpw_transition')"); + +# --- the primary crosses into double_writes via a restart ---------------- + +my $warn_offset = -s $standby->logfile; +$primary->append_conf('postgresql.conf', + 'io_torn_pages_protection = double_writes'); +$primary->restart; + +$primary->safe_psql('postgres', + 'INSERT INTO dwb_fpw SELECT g FROM generate_series(1001, 2000) g'); +$primary->safe_psql('postgres', 'CHECKPOINT'); +# a second image-less checkpoint proves the warning below does not repeat +$primary->safe_psql('postgres', 'CHECKPOINT'); +$primary->wait_for_catchup($standby); + +# --- the DWB-less standby warns exactly once ----------------------------- + +my $log = slurp_file($standby->logfile, $warn_offset); +my @warnings = $log =~ + /(replaying WAL generated without full page images, but this server does not use the double write buffer)/g; +is(scalar(@warnings), 1, + 'standby without a ring of its own warns exactly once per startup'); + +# --- the open backup cannot be closed cleanly ---------------------------- + +my $stop_offset = -s $standby->logfile; +my ($out, $errored) = $backer->query('SELECT * FROM pg_backup_stop()'); +ok($errored, 'pg_backup_stop() on the standby fails across the transition'); +ok( $standby->log_contains( + qr/WAL generated without full page images was replayed during online backup/, + $stop_offset), + '... naming the replayed image-less WAL'); +$backer->quit; + +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index fc3b26d3044a0..ff62a855bdc9f 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -760,8 +760,9 @@ test_dwb_open_stale(PG_FUNCTION_ARGS) stale_idx = pg_atomic_read_u32(&DWBCtl->open_batch_idx[DWB_WCLASS_EVICTION]); /* - * Acquire one slot: the fetch_add bounces on SEAL_BIT and reopens the - * lowest FREE index — the same index again, as a new live incarnation. + * Acquire one slot: the reservation CAS bounces on SEAL_BIT and reopens + * the lowest FREE index — the same index again, as a new live + * incarnation. */ tag = make_tag(1, 92000, 0); DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); From a1c999ffedcbe009e514ba11b7c9d6f5fe99daa0 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 25 Jul 2026 09:07:42 +0300 Subject: [PATCH 12/52] Trim duplication left by the Stage 3 review round The replace-guard in DWBOpenNewBatch computes staleness into a flag instead of duplicating the release-and-return sequence per branch. decide_file_action() drops the refusal of a regular-file pg_dwb on the source: the source's ring is never read or copied, so the check protected nothing (the target side is covered by checkTargetDwb()). The rewind test hoists the six copies of the pg_rewind invocation into one array. No behavior change on any path a server can reach. --- src/backend/storage/dwb/dwb.c | 24 +++++-------- src/bin/pg_rewind/filemap.c | 9 ++--- src/test/modules/test_dwb/t/007_rewind.pl | 43 ++++++----------------- 3 files changed, 22 insertions(+), 54 deletions(-) diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 15d8ad96812f1..077cc3134b0c8 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -313,28 +313,22 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) */ { uint32 cur = pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]); + bool stale = (cur == old_idx); - if (cur != old_idx) + if (stale && cur != DWB_INVALID_BATCH) + { + uint32 nsi = pg_atomic_read_u32(&DWBCtl->batches[cur].next_slot_idx); + + stale = (nsi & DWB_SEAL_BIT) || + (nsi & DWB_WCLASS_BIT) != DWBWClassBit(wclass); + } + if (!stale) { LWLockRelease(DWBRingOpenLock); DWBStagingRelease(staging_idx); ConditionVariableCancelSleep(); return; } - if (cur != DWB_INVALID_BATCH) - { - uint32 nsi = pg_atomic_read_u32(&DWBCtl->batches[cur].next_slot_idx); - - if (!(nsi & DWB_SEAL_BIT) && - (nsi & DWB_WCLASS_BIT) == DWBWClassBit(wclass)) - { - /* still our live open batch */ - LWLockRelease(DWBRingOpenLock); - DWBStagingRelease(staging_idx); - ConditionVariableCancelSleep(); - return; - } - } } /* diff --git a/src/bin/pg_rewind/filemap.c b/src/bin/pg_rewind/filemap.c index a53bf86375619..4ebda55e2df32 100644 --- a/src/bin/pg_rewind/filemap.c +++ b/src/bin/pg_rewind/filemap.c @@ -724,16 +724,11 @@ decide_file_action(file_entry_t *entry) * not at all, and the server (re)creates it lazily, see DWBCreateRing(). * Its contents match the exclusion filters and are removed from the * target below. The target's entry and ring contents are validated up - * front by checkTargetDwb() before the traversal; on the source side only - * a regular file can show up here, and it never has a legitimate reason - * to exist. + * front by checkTargetDwb() before the traversal; the source's entry + * needs no validation, its ring is never used. */ if (strcmp(path, "pg_dwb") == 0) - { - if (entry->source_exists && entry->source_type == FILE_TYPE_REGULAR) - pg_fatal("\"%s\" in source is not a directory or symbolic link", path); return FILE_ACTION_NONE; - } /* Skip macOS system files */ if (strstr(path, ".DS_Store") != NULL) diff --git a/src/test/modules/test_dwb/t/007_rewind.pl b/src/test/modules/test_dwb/t/007_rewind.pl index da3662f3eabc9..296985f2c1810 100644 --- a/src/test/modules/test_dwb/t/007_rewind.pl +++ b/src/test/modules/test_dwb/t/007_rewind.pl @@ -85,6 +85,11 @@ $node_b->stop('fast'); +my @rewind_from_b = ( + 'pg_rewind', + '--target-pgdata' => $node_a->data_dir, + '--source-pgdata' => $node_b->data_dir); + # --- a garbage entry in place of pg_dwb is refused ----------------------- # Anything but a directory, a symlink or nothing at all would survive the @@ -96,11 +101,7 @@ rename($dwb_path, $stash) or BAIL_OUT("could not move $dwb_path: $!"); append_to_file($dwb_path, "not a ring\n"); command_fails_like( - [ - 'pg_rewind', - '--target-pgdata' => $node_a->data_dir, - '--source-pgdata' => $node_b->data_dir - ], + [@rewind_from_b], qr/"pg_dwb" in target is not a directory or symbolic link/, 'pg_rewind refuses a regular file in place of pg_dwb'); unlink($dwb_path) or BAIL_OUT("could not remove $dwb_path: $!"); @@ -114,11 +115,7 @@ POSIX::mkfifo($dwb_path, 0700) or BAIL_OUT("could not create FIFO $dwb_path: $!"); command_fails_like( - [ - 'pg_rewind', - '--target-pgdata' => $node_a->data_dir, - '--source-pgdata' => $node_b->data_dir - ], + [@rewind_from_b], qr/"pg_dwb" in target is not a directory or symbolic link/, 'pg_rewind refuses a FIFO in place of pg_dwb'); unlink($dwb_path) or BAIL_OUT("could not remove $dwb_path: $!"); @@ -127,11 +124,7 @@ symlink('/nonexistent/dwb_ring_target', $dwb_path) or BAIL_OUT("could not symlink $dwb_path: $!"); command_fails_like( - [ - 'pg_rewind', - '--target-pgdata' => $node_a->data_dir, - '--source-pgdata' => $node_b->data_dir - ], + [@rewind_from_b], qr/"pg_dwb" in target is a symbolic link that does not point to a directory/, 'pg_rewind refuses a broken pg_dwb symlink'); unlink($dwb_path) or BAIL_OUT("could not remove $dwb_path: $!"); @@ -140,11 +133,7 @@ symlink("$stash.file", $dwb_path) or BAIL_OUT("could not symlink $dwb_path: $!"); command_fails_like( - [ - 'pg_rewind', - '--target-pgdata' => $node_a->data_dir, - '--source-pgdata' => $node_b->data_dir - ], + [@rewind_from_b], qr/"pg_dwb" in target is a symbolic link that does not point to a directory/, 'pg_rewind refuses a pg_dwb symlink to a regular file'); unlink($dwb_path) or BAIL_OUT("could not remove $dwb_path: $!"); @@ -156,11 +145,7 @@ POSIX::mkfifo("$dwb_path/control", 0700) or BAIL_OUT("could not create FIFO $dwb_path/control: $!"); command_fails_like( - [ - 'pg_rewind', - '--target-pgdata' => $node_a->data_dir, - '--source-pgdata' => $node_b->data_dir - ], + [@rewind_from_b], qr!"pg_dwb/control" in target is not a regular file!, 'pg_rewind refuses a FIFO inside pg_dwb'); unlink("$dwb_path/control") @@ -198,13 +183,7 @@ or BAIL_OUT("could not symlink $b_dwb: $!"); } -command_ok( - [ - 'pg_rewind', - '--target-pgdata' => $node_a->data_dir, - '--source-pgdata' => $node_b->data_dir - ], - 'pg_rewind from a stopped source succeeds'); +command_ok([@rewind_from_b], 'pg_rewind from a stopped source succeeds'); ok(-d $node_a->data_dir . '/pg_dwb', 'the target still has a pg_dwb directory'); { From 114ec68041e1f117be3b0e670e0c4a71a51a3e59 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 25 Jul 2026 12:52:19 +0300 Subject: [PATCH 13/52] Repair torn pages at startup and record the mode in pg_control (Stage 4) Before WAL replay, DWBStartup now runs an eager apply-pass over the ring unless it was cleanly closed: candidate slots (valid batch header, meta_crc, current generation, image_crc, not ABORTED) are deduplicated per page keeping the highest LSN, and a data page is rewritten from its slot copy when it fails verification or is older than the copy; repaired forks are fsynced before the generation bump. This is the only repair path, and it covers pages replay never reads, such as hint-bit-only pages logged as XLOG_FPI_FOR_HINT without an image. The ring's control file gains a RING_CLEAN marker, written at the tail of a clean shutdown once the ring is fully retired and cleared when the ring reopens. The apply-pass keys on pg_control OR the marker: a standby's shutdown restartpoint can be skipped entirely, leaving segment fsyncs pending behind a clean pg_control. The same marker guards mode downgrades: with an uncleanly closed ring, a start under full_pages or off is refused until one clean double_writes run applies it. A start from a restored base backup (backup_label present, or backupStartPoint still set after a crash mid-backup-recovery) never applies a ring shipped by a third-party backup tool: the generation and LSN defences both pass honestly there, and an apply would push pages from the backup's future into a PITR target. The ring contents are discarded and recreated cold, in the non-DWB modes too. A geometry change now applies the old ring first and recreates it under the new geometry instead of refusing to start. io_torn_pages_protection is recorded in pg_control and in XLOG_PARAMETER_CHANGE, like wal_level: a server expecting full-page protection refuses to replay WAL generated without page images (CheckRequiredParameterValues FATAL, replacing the once-per-startup WARNING), the standby backup guards name the primary's actual mode, and pg_rewind reads a live source's mode from its pg_control up front, before ensureCleanShutdown can touch the target. On a crashed standby the apply-pass raises minRecoveryPoint to the highest applied LSN as a belt-and-braces enforcement of what the write path already guarantees. Tests: new t/010_recovery.pl (torn hint page, stale-but-valid page repaired by LSN, idempotent re-apply, clean-start skip, past-generation slot never applied); 009 re-pointed at the legacy full_page_writes=off transition plus the incompatible-standby FATAL; 006 plants a foreign ring into a restore; 007 rewinds a crashed target through the single-user run; 008 covers the downgrade guard; 001 the geometry recreate. --- src/backend/access/rmgrdesc/xlogdesc.c | 6 +- src/backend/access/transam/xlog.c | 126 ++++- src/backend/storage/dwb/dwb_recovery.c | 494 +++++++++++++++++- src/bin/pg_controldata/pg_controldata.c | 2 + src/bin/pg_resetwal/pg_resetwal.c | 2 + src/bin/pg_rewind/libpq_source.c | 18 +- src/bin/pg_rewind/pg_rewind.c | 23 + src/include/access/xlog_internal.h | 3 +- src/include/catalog/pg_control.h | 34 +- src/include/storage/dwb.h | 26 +- src/test/modules/test_dwb/meson.build | 1 + src/test/modules/test_dwb/t/001_dwb.pl | 38 +- src/test/modules/test_dwb/t/006_backup.pl | 45 ++ src/test/modules/test_dwb/t/007_rewind.pl | 8 +- src/test/modules/test_dwb/t/008_modes.pl | 36 ++ .../modules/test_dwb/t/009_fpw_transition.pl | 92 +++- src/test/modules/test_dwb/t/010_recovery.pl | 206 ++++++++ src/tools/pgindent/typedefs.list | 2 + 18 files changed, 1046 insertions(+), 116 deletions(-) create mode 100644 src/test/modules/test_dwb/t/010_recovery.pl diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c index 58040f28656fc..f5fb54d1bf4a0 100644 --- a/src/backend/access/rmgrdesc/xlogdesc.c +++ b/src/backend/access/rmgrdesc/xlogdesc.c @@ -124,7 +124,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record) appendStringInfo(buf, "max_connections=%d max_worker_processes=%d " "max_wal_senders=%d max_prepared_xacts=%d " "max_locks_per_xact=%d wal_level=%s " - "wal_log_hints=%s track_commit_timestamp=%s", + "wal_log_hints=%s track_commit_timestamp=%s " + "io_torn_pages_protection=%s", xlrec.MaxConnections, xlrec.max_worker_processes, xlrec.max_wal_senders, @@ -132,7 +133,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record) xlrec.max_locks_per_xact, wal_level_str, xlrec.wal_log_hints ? "on" : "off", - xlrec.track_commit_timestamp ? "on" : "off"); + xlrec.track_commit_timestamp ? "on" : "off", + DWBProtectionModeName(xlrec.io_torn_pages_protection)); } else if (info == XLOG_FPW_CHANGE) { diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index b77351947f711..73c1ba2bbc40e 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -4230,6 +4230,7 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version) ControlFile->wal_level = wal_level; ControlFile->wal_log_hints = wal_log_hints; ControlFile->track_commit_timestamp = track_commit_timestamp; + ControlFile->io_torn_pages_protection = io_torn_pages_protection; ControlFile->data_checksum_version = data_checksum_version; } @@ -5437,6 +5438,26 @@ CheckRequiredParameterValues(void) errhint("Use a backup taken after setting \"wal_level\" to higher than \"minimal\"."))); } + /* + * A server that believes full page images protect it must not replay WAL + * generated without them: its own crash would leave torn data pages that + * neither this WAL nor its configuration can repair. A local double + * write buffer repairs its own torn pages instead, and under "off" the + * user has explicitly waived the protection, so only the "full_pages" + * expectation is refused. + */ + if (ArchiveRecoveryRequested && + ControlFile->io_torn_pages_protection != DWB_PROTECT_FULL_PAGES && + io_torn_pages_protection == DWB_PROTECT_FULL_PAGES) + { + ereport(FATAL, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("WAL was generated with \"io_torn_pages_protection=%s\", cannot continue recovering with \"io_torn_pages_protection=full_pages\"", + DWBProtectionModeName(ControlFile->io_torn_pages_protection)), + errdetail("The WAL carries no full page images, so a crash of this server would leave torn data pages that nothing can repair."), + errhint("Set \"io_torn_pages_protection\" to \"double_writes\" on this server, or to \"full_pages\" on the server that generated the WAL."))); + } + /* * For Hot Standby, the WAL must be generated with 'replica' mode, and we * must have at least as many backend slots as the primary. @@ -5597,12 +5618,46 @@ StartupXLOG(void) didCrash = false; /* - * Create or validate the double write buffer ring and durably bump its - * generation before any of its slots can be written. (The Stage 4 - * apply-pass over the previous generation will run here, before WAL - * recovery is initialized.) + * Create or validate the double write buffer ring, repair torn data pages + * from it if the previous run did not close it cleanly, and durably bump + * its generation before any of its slots can be written. This runs before + * InitWalRecovery: the repairs establish the base that WAL replay + * advances from, and the backup_label file (a "restoring from base + * backup" indicator, together with backupStartPoint) is still in place + * here. */ - DWBStartup(); + { + bool restoring_backup; + XLogRecPtr dwb_applied_upto; + + restoring_backup = + !XLogRecPtrIsInvalid(ControlFile->backupStartPoint) || + access(BACKUP_LABEL_FILE, F_OK) == 0; + + dwb_applied_upto = DWBStartup(didCrash, restoring_backup); + + /* + * On a crashed standby, consistency must not be declared before the + * local WAL covers the repaired pages. The write path guarantees + * minRecoveryPoint already does — FlushBuffer's XLogFlush advances + * it durably before the page can enter the ring — so this raise is + * expected to be a no-op; it stays as a belt-and-braces enforcement + * of the invariant. The timeline is left alone: any LSN the ring can + * hold lies on a timeline minRecoveryPoint has already seen, by the + * same write-path argument. + */ + if (ControlFile->state == DB_IN_ARCHIVE_RECOVERY && + !XLogRecPtrIsInvalid(ControlFile->minRecoveryPoint) && + dwb_applied_upto > ControlFile->minRecoveryPoint) + { + elog(LOG, "raising minimum recovery point to %X/%X to cover pages repaired from the double write buffer", + LSN_FORMAT_ARGS(dwb_applied_upto)); + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + ControlFile->minRecoveryPoint = dwb_applied_upto; + UpdateControlFile(); + LWLockRelease(ControlFileLock); + } + } /* * Prepare for WAL recovery if needed. @@ -6690,6 +6745,15 @@ ShutdownXLOG(int code, Datum arg) CreateCheckPoint(CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_IMMEDIATE); } + + /* + * Retire what is left in the double write buffer ring — a shutdown + * restartpoint can be skipped entirely, leaving segment fsyncs pending — + * and mark the ring cleanly closed. The next start can then skip the + * apply-pass, and a start under a different io_torn_pages_protection mode + * is legal. + */ + DWBMarkCleanShutdown(); } /* @@ -8166,7 +8230,8 @@ XLogReportParameters(void) max_wal_senders != ControlFile->max_wal_senders || max_prepared_xacts != ControlFile->max_prepared_xacts || max_locks_per_xact != ControlFile->max_locks_per_xact || - track_commit_timestamp != ControlFile->track_commit_timestamp) + track_commit_timestamp != ControlFile->track_commit_timestamp || + io_torn_pages_protection != ControlFile->io_torn_pages_protection) { /* * The change in number of backend slots doesn't need to be WAL-logged @@ -8175,7 +8240,9 @@ XLogReportParameters(void) * values in pg_control either if wal_level=minimal, but seems better * to keep them up-to-date to avoid confusion. */ - if (wal_level != ControlFile->wal_level || XLogIsNeeded()) + if (wal_level != ControlFile->wal_level || + io_torn_pages_protection != ControlFile->io_torn_pages_protection || + XLogIsNeeded()) { xl_parameter_change xlrec; XLogRecPtr recptr; @@ -8188,6 +8255,7 @@ XLogReportParameters(void) xlrec.wal_level = wal_level; xlrec.wal_log_hints = wal_log_hints; xlrec.track_commit_timestamp = track_commit_timestamp; + xlrec.io_torn_pages_protection = io_torn_pages_protection; XLogBeginInsert(); XLogRegisterData(&xlrec, sizeof(xlrec)); @@ -8206,6 +8274,7 @@ XLogReportParameters(void) ControlFile->wal_level = wal_level; ControlFile->wal_log_hints = wal_log_hints; ControlFile->track_commit_timestamp = track_commit_timestamp; + ControlFile->io_torn_pages_protection = io_torn_pages_protection; UpdateControlFile(); LWLockRelease(ControlFileLock); @@ -8307,15 +8376,13 @@ UpdateFullPageWrites(void) * off) emits no XLOG_FPW_CHANGE — its checkpoints are the only replayed * evidence of the change (see UpdateFullPageWrites). * - * Also the place to warn, once per startup process, when this server - * replays image-less WAL without running a double write buffer of its own: - * a crash would then leave torn data pages that nothing can repair. + * A server that replays image-less WAL while expecting full-page protection + * is refused outright by CheckRequiredParameterValues, keyed on the + * generating server's io_torn_pages_protection in pg_control. */ static void XLogTrackFullPageWritesDisabled(XLogReaderState *record, bool fpw) { - static bool warned = false; - if (fpw) return; @@ -8323,15 +8390,6 @@ XLogTrackFullPageWritesDisabled(XLogReaderState *record, bool fpw) if (XLogCtl->lastFpwDisableRecPtr < record->ReadRecPtr) XLogCtl->lastFpwDisableRecPtr = record->ReadRecPtr; SpinLockRelease(&XLogCtl->info_lck); - - if (!warned && !DWBIsEnabled()) - { - ereport(WARNING, - (errmsg("replaying WAL generated without full page images, but this server does not use the double write buffer"), - errdetail("Torn data pages left by a crash cannot be repaired by this WAL or by this server's configuration."), - errhint("Set \"io_torn_pages_protection\" to \"double_writes\" on this server, or to \"full_pages\" on the server that generated the WAL."))); - warned = true; - } } /* @@ -8654,6 +8712,7 @@ xlog_redo(XLogReaderState *record) ControlFile->max_locks_per_xact = xlrec.max_locks_per_xact; ControlFile->wal_level = xlrec.wal_level; ControlFile->wal_log_hints = xlrec.wal_log_hints; + ControlFile->io_torn_pages_protection = xlrec.io_torn_pages_protection; /* * Update minRecoveryPoint to ensure that if recovery is aborted, we @@ -8993,6 +9052,7 @@ do_pg_backup_start(const char *backupidstr, bool fast, List **tablespaces, do { bool checkpointfpw; + int primary_iotpp; /* * Force a CHECKPOINT. Aside from being necessary to prevent torn @@ -9026,6 +9086,7 @@ do_pg_backup_start(const char *backupidstr, bool fast, List **tablespaces, state->startpoint = ControlFile->checkPointCopy.redo; state->starttli = ControlFile->checkPointCopy.ThisTimeLineID; checkpointfpw = ControlFile->checkPointCopy.fullPageWrites; + primary_iotpp = ControlFile->io_torn_pages_protection; LWLockRelease(ControlFileLock); if (backup_started_in_recovery) @@ -9046,9 +9107,11 @@ do_pg_backup_start(const char *backupidstr, bool fast, List **tablespaces, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("WAL generated without full page images was replayed " "since last restartpoint"), - errdetail("The primary does not write full page images: it runs " - "io_torn_pages_protection = \"double_writes\" or \"off\", " - "or full_page_writes is disabled."), + primary_iotpp != DWB_PROTECT_FULL_PAGES + ? errdetail("The primary runs \"io_torn_pages_protection\" = \"%s\" " + "and does not write full page images.", + DWBProtectionModeName(primary_iotpp)) + : errdetail("The primary has \"full_page_writes\" disabled."), errhint("A backup taken on a standby needs full page images in the " "replayed WAL; the primary's double write buffer cannot " "substitute for them. Set io_torn_pages_protection = " @@ -9333,6 +9396,7 @@ do_pg_backup_stop(BackupState *state, bool waitforarchive) if (backup_stopped_in_recovery) { XLogRecPtr recptr; + int primary_iotpp; /* * Check to see if all WAL replayed during online backup contain @@ -9342,13 +9406,23 @@ do_pg_backup_stop(BackupState *state, bool waitforarchive) recptr = XLogCtl->lastFpwDisableRecPtr; SpinLockRelease(&XLogCtl->info_lck); + LWLockAcquire(ControlFileLock, LW_SHARED); + primary_iotpp = ControlFile->io_torn_pages_protection; + LWLockRelease(ControlFileLock); + if (state->startpoint <= recptr) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("WAL generated without full page images was replayed " "during online backup"), - errdetail("The backup being taken on the standby is corrupt " - "and should not be used."), + primary_iotpp != DWB_PROTECT_FULL_PAGES + ? errdetail("The backup being taken on the standby is corrupt " + "and should not be used: the primary runs " + "\"io_torn_pages_protection\" = \"%s\".", + DWBProtectionModeName(primary_iotpp)) + : errdetail("The backup being taken on the standby is corrupt " + "and should not be used: the primary has " + "\"full_page_writes\" disabled."), errhint("A backup taken on a standby needs full page images in the " "replayed WAL; the primary's double write buffer cannot " "substitute for them. Set io_torn_pages_protection = " diff --git a/src/backend/storage/dwb/dwb_recovery.c b/src/backend/storage/dwb/dwb_recovery.c index 124c6ab6bc03c..e86712b8cce82 100644 --- a/src/backend/storage/dwb/dwb_recovery.c +++ b/src/backend/storage/dwb/dwb_recovery.c @@ -1,14 +1,25 @@ /*------------------------------------------------------------------------- * * dwb_recovery.c - * Startup-time handling of the short-lived double write buffer ring. + * Startup-time handling of the short-lived double write buffer ring: + * the apply-pass that repairs torn data pages, and the durable + * generation protocol around it. * * On every start (clean, unclean or cold) the durable generation in * pg_dwb/control is bumped BEFORE the ring opens for new writes, so slots * left behind by the previous run can never masquerade as current after a - * future crash. Order: read G -> (unclean start, Stage 4) apply-pass over + * future crash. Order: read G -> (ring not cleanly closed) apply-pass over * generation G + fsync -> durable control.generation := G+1 -> open ring. * + * The apply-pass runs before WAL replay and repairs the data files + * directly: a candidate slot must carry a valid meta_crc, the current + * generation and a valid image_crc; candidates are deduplicated per page + * keeping the highest LSN, and a page is rewritten from its slot copy when + * the on-disk version fails verification or carries an older LSN. This is + * the only repair path — the runtime read path never consults the ring — + * and it covers pages replay never reads, such as hint-bit-only pages + * logged as XLOG_FPI_FOR_HINT without an image. + * * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * @@ -19,18 +30,344 @@ */ #include "postgres.h" +#include +#include +#include + #include "access/xlog.h" +#include "pgstat.h" +#include "storage/bufpage.h" #include "storage/dwb.h" +#include "storage/fd.h" +#include "storage/smgr.h" +#include "utils/hsearch.h" +#include "utils/wait_event.h" + +/* dedup table entry: the best candidate slot seen for one page */ +typedef struct DWBApplyCandidate +{ + BufferTag tag; /* hash key */ + XLogRecPtr lsn; + uint64 batch_id; /* tie-breaker for equal LSNs */ + uint32 batch_idx; + uint32 slot_idx; +} DWBApplyCandidate; + +/* one fork the apply-pass has written to and must fsync */ +typedef struct DWBAppliedFork +{ + RelFileLocator rlocator; + ForkNumber forknum; +} DWBAppliedFork; + +static XLogRecPtr DWBApplyPass(const DWBControlFileData *control); +static void DWBWipeRing(void); +static bool DWBRingIsQuiescent(void); /* - * Called from StartupXLOG before WAL replay. Creates or validates the - * ring, enforces data checksums, performs the durable generation bump and - * publishes ring_generation for the leaders' slot metas. + * Read one batch file's page image into an aligned buffer. The apply-pass + * variant of DWBReadSlotImage: geometry comes from the on-disk control + * file, not the GUCs, and failures are plain ERRORs (startup context, no + * critical section). */ -void -DWBStartup(void) +static void +DWBApplyReadImage(int fd, const char *path, uint32 batch_pages, + uint32 slot_idx, char *dst) +{ + off_t off = DWBMetaRegionSize(batch_pages) + + (off_t) slot_idx * BLCKSZ; + ssize_t r; + + pgstat_report_wait_start(WAIT_EVENT_DWB_BATCH_READ); + r = pg_pread(fd, dst, BLCKSZ, off); + pgstat_report_wait_end(); + if (r != BLCKSZ) + { + if (r < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", path))); + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("could not read file \"%s\": read %zd of %d", + path, r, BLCKSZ))); + } +} + +/* + * The eager repair pass (3.8 of the design plan). Scans the whole ring, + * selects candidate slots of the current generation, dedups them per page + * and rewrites the data pages that are torn or older than their copy. + * Returns the highest LSN actually applied, or InvalidXLogRecPtr. + * + * Runs in the startup process before WAL replay, strictly read-only with + * respect to the ring: a crash in the middle leaves control.generation + * untouched and the next start simply repeats the pass (already-repaired + * pages then carry disk_lsn >= slot_lsn and are skipped). + */ +static XLogRecPtr +DWBApplyPass(const DWBControlFileData *control) +{ + Size meta_region = DWBMetaRegionSize(control->batch_pages); + char *meta_buf = palloc(meta_region); + char *image_buf = palloc_aligned(BLCKSZ, PG_IO_ALIGN_SIZE, 0); + char *disk_buf = palloc_aligned(BLCKSZ, PG_IO_ALIGN_SIZE, 0); + HASHCTL info; + HTAB *candidates; + HASH_SEQ_STATUS seq; + DWBApplyCandidate *cand; + DWBAppliedFork *applied_forks; + int n_applied_forks = 0; + int n_candidates = 0; + int n_applied = 0; + XLogRecPtr applied_upto = InvalidXLogRecPtr; + + info.keysize = sizeof(BufferTag); + info.entrysize = sizeof(DWBApplyCandidate); + candidates = hash_create("DWB apply-pass candidates", + (long) control->num_batches * control->batch_pages, + &info, + HASH_ELEM | HASH_BLOBS); + + /* + * Scan every batch file with the geometry recorded in control. A batch + * or slot that fails any local validity check is skipped, not an error: + * by the write protocol a torn or half-written slot means the + * corresponding data-file write never started, so the disk holds an older + * durable version that replay can advance from. + */ + for (uint32 batch_idx = 0; batch_idx < control->num_batches; batch_idx++) + { + char path[MAXPGPATH]; + int fd; + ssize_t r; + DWBBatchHeader hdr; + DWSlotMeta *metas; + + snprintf(path, sizeof(path), DWB_DIR "/batch_%04u", batch_idx); + fd = OpenTransientFile(path, O_RDONLY | PG_BINARY); + if (fd < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", path))); + + pgstat_report_wait_start(WAIT_EVENT_DWB_BATCH_READ); + r = pg_pread(fd, meta_buf, meta_region, 0); + pgstat_report_wait_end(); + if (r != (ssize_t) meta_region) + { + if (r < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", path))); + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("could not read file \"%s\": read %zd of %zu", + path, r, meta_region))); + } + + memcpy(&hdr, meta_buf, sizeof(DWBBatchHeader)); + if (hdr.magic != DWB_BATCH_MAGIC || + hdr.version != DWB_VERSION || + hdr.n_slots > control->batch_pages || + !EQ_CRC32C(hdr.crc, DWBBatchHeaderCrc(&hdr))) + { + /* never sealed, or torn mid-write: nothing durable depends on it */ + CloseTransientFile(fd); + continue; + } + + metas = (DWSlotMeta *) (meta_buf + sizeof(DWBBatchHeader)); + for (uint32 slot_idx = 0; slot_idx < hdr.n_slots; slot_idx++) + { + DWSlotMeta *meta = &metas[slot_idx]; + DWBApplyCandidate *entry; + bool found; + + if (!EQ_CRC32C(meta->meta_crc, DWBSlotMetaCrc(meta))) + continue; + if (meta->generation != control->generation) + continue; + if (meta->flags & DWB_SLOT_ABORTED) + continue; + + DWBApplyReadImage(fd, path, control->batch_pages, slot_idx, + image_buf); + if (!EQ_CRC32C(meta->image_crc, DWBImageCrc(image_buf))) + continue; + + n_candidates++; + entry = hash_search(candidates, &meta->tag, HASH_ENTER, &found); + if (found && + (entry->lsn > meta->page_lsn || + (entry->lsn == meta->page_lsn && + entry->batch_id > hdr.batch_id))) + continue; + entry->lsn = meta->page_lsn; + entry->batch_id = hdr.batch_id; + entry->batch_idx = batch_idx; + entry->slot_idx = slot_idx; + } + + if (CloseTransientFile(fd) != 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", path))); + } + + /* + * Repair. For each winning candidate, rewrite the data page when the + * on-disk version fails verification or is older than the copy. A + * dropped relation (no file) or a truncated one (block beyond EOF) is + * skipped: there is nothing to repair and replay or a replayed truncate + * drives the final state. + */ + applied_forks = palloc(sizeof(DWBAppliedFork) * + hash_get_num_entries(candidates)); + + hash_seq_init(&seq, candidates); + while ((cand = hash_seq_search(&seq)) != NULL) + { + RelFileLocator rlocator = BufTagGetRelFileLocator(&cand->tag); + ForkNumber forknum = BufTagGetForkNum(&cand->tag); + BlockNumber blkno = cand->tag.blockNum; + SMgrRelation reln; + char path[MAXPGPATH]; + int fd; + bool known_fork; + + reln = smgropen(rlocator, INVALID_PROC_NUMBER); + if (!smgrexists(reln, forknum)) + continue; + if (blkno >= smgrnblocks(reln, forknum)) + continue; + + smgrread(reln, forknum, blkno, disk_buf); + if (PageIsVerified((Page) disk_buf, blkno, PIV_LOG_LOG, NULL) && + PageGetLSN((Page) disk_buf) >= cand->lsn) + continue; + + /* re-read the winning copy; the scan buffer is long overwritten */ + snprintf(path, sizeof(path), DWB_DIR "/batch_%04u", cand->batch_idx); + fd = OpenTransientFile(path, O_RDONLY | PG_BINARY); + if (fd < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", path))); + DWBApplyReadImage(fd, path, control->batch_pages, cand->slot_idx, + image_buf); + if (CloseTransientFile(fd) != 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", path))); + + elog(DEBUG1, "double write buffer recovery: restoring page %u of relation %u/%u/%u fork %d from batch %u slot %u (LSN %X/%X)", + blkno, rlocator.spcOid, rlocator.dbOid, rlocator.relNumber, + forknum, cand->batch_idx, cand->slot_idx, + LSN_FORMAT_ARGS(cand->lsn)); + + smgrwrite(reln, forknum, blkno, image_buf, true); + + n_applied++; + if (cand->lsn > applied_upto) + applied_upto = cand->lsn; + + known_fork = false; + for (int i = 0; i < n_applied_forks; i++) + { + if (RelFileLocatorEquals(applied_forks[i].rlocator, rlocator) && + applied_forks[i].forknum == forknum) + { + known_fork = true; + break; + } + } + if (!known_fork) + { + applied_forks[n_applied_forks].rlocator = rlocator; + applied_forks[n_applied_forks].forknum = forknum; + n_applied_forks++; + } + } + + /* make the repairs durable before the generation moves on */ + for (int i = 0; i < n_applied_forks; i++) + smgrimmedsync(smgropen(applied_forks[i].rlocator, + INVALID_PROC_NUMBER), + applied_forks[i].forknum); + + ereport(LOG, + (errmsg("double write buffer recovery: %d of %d candidate pages restored, generation " UINT64_FORMAT, + n_applied, n_candidates, control->generation))); + + hash_destroy(candidates); + pfree(applied_forks); + pfree(meta_buf); + pfree(image_buf); + pfree(disk_buf); + + return applied_upto; +} + +/* + * Durably remove the contents of pg_dwb/, keeping the directory (or the + * symlink to it) in place. Used when a restored backup ships a foreign + * ring and when the geometry GUCs changed. + */ +static void +DWBWipeRing(void) +{ + struct stat st; + + if (lstat(DWB_DIR, &st) < 0) + { + if (errno == ENOENT) + return; + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not stat directory \"%s\": %m", DWB_DIR))); + } + + if (!rmtree(DWB_DIR, false)) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not remove contents of directory \"%s\"", + DWB_DIR))); + fsync_fname(DWB_DIR, true); +} + +/* + * Called from StartupXLOG before WAL recovery is initialized. Creates or + * validates the ring, enforces data checksums, repairs torn data pages + * from the ring unless it was cleanly closed, performs the durable + * generation bump and publishes ring_generation for the leaders' slot + * metas. Returns the highest LSN the apply-pass wrote to a data file, or + * InvalidXLogRecPtr. + * + * unclean_start is the caller's pg_control verdict (neither DB_SHUTDOWNED + * state). The apply-pass additionally keys on the ring's own RING_CLEAN + * marker: a standby's shutdown restartpoint can be skipped entirely, + * leaving retirement fsyncs pending, and then only the marker knows the + * ring still covers data writes that may not have reached disk. + * + * restoring_backup means the data directory is a restored base backup + * (backup_label present, or pg_control still carries backupStartPoint + * after a crash mid-backup-recovery). Any ring found in that case was + * shipped by a third-party backup tool and must not be applied: its slots + * carry the restored control's own generation, and the restored data files + * are legitimately older than the slot copies, so both staleness defences + * pass — an unguarded apply would push pages from the future of the backup + * into a PITR target. The WAL of the backup window carries forced full + * page images instead, so the ring is not needed for this recovery; it is + * wiped and recreated cold. + */ +XLogRecPtr +DWBStartup(bool unclean_start, bool restoring_backup) { DWBControlFileData control; + XLogRecPtr applied_upto = InvalidXLogRecPtr; + bool created = false; + bool need_apply; if (!DWBIsEnabled()) { @@ -43,7 +380,37 @@ DWBStartup(void) ereport(LOG, (errmsg("torn page protection is disabled (io_torn_pages_protection = \"off\")"), errdetail("WAL carries no full page images; \"full_page_writes\" is ignored in this mode."))); - return; + + if (restoring_backup) + { + /* + * A foreign ring shipped in a restored backup is dangerous even + * lying dormant: a much later switch to double_writes would find + * it with a plausible control file. Discard it now. + */ + DWBWipeRing(); + return InvalidXLogRecPtr; + } + + /* + * Mode-downgrade guard: a ring that was not cleanly closed may hold + * repairs of torn data pages that only a double_writes start can + * apply. This intentionally does not consider pg_control: a clean + * server shutdown that failed to retire the ring (e.g. a soft fsync + * failure under data_sync_retry) leaves RING_CLEAN unset, and the + * pending data writes it covers are exactly as unprotected. + */ + if (DWBReadControlFile(&control, true) && + (control.flags & DWB_CONTROL_RING_CLEAN) == 0) + ereport(FATAL, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("the double write buffer ring was not cleanly shut down, cannot start with \"io_torn_pages_protection=%s\"", + DWBProtectionModeName(io_torn_pages_protection)), + errdetail("The ring in \"%s\" may hold repairs of torn data pages that have not been applied.", + DWB_DIR), + errhint("Start the server once with \"io_torn_pages_protection=double_writes\" and shut it down cleanly, or remove \"%s\" if you accept the risk of torn data pages.", + DWB_DIR))); + return InvalidXLogRecPtr; } /* 3.1.7: a torn page with an intact header must never pass unnoticed */ @@ -53,34 +420,57 @@ DWBStartup(void) errmsg("io_torn_pages_protection = \"double_writes\" requires data checksums"), errhint("Enable checksums with initdb -k or pg_checksums."))); + if (restoring_backup) + { + ereport(LOG, + (errmsg("discarding double write buffer ring contents restored from a base backup"))); + DWBWipeRing(); + } + if (!DWBReadControlFile(&control, true)) { /* cold start: no ring yet */ DWBCreateRing(); + created = true; if (!DWBReadControlFile(&control, false)) pg_unreachable(); } - else if (control.num_batches != (uint32) dwb_num_batches || - control.batch_pages != (uint32) dwb_batch_pages) + + /* + * The pg_control verdict alone is not enough: see the RING_CLEAN + * discussion in the header comment. A freshly created ring has nothing + * to apply even though its marker is unset. + */ + need_apply = !created && + (unclean_start || (control.flags & DWB_CONTROL_RING_CLEAN) == 0); + + if (!created && + (control.num_batches != (uint32) dwb_num_batches || + control.batch_pages != (uint32) dwb_batch_pages)) { /* - * Geometry GUCs define the on-disk layout. Re-creating the ring - * under a changed geometry must not skip the apply-pass over the old - * ring, so it is deferred to Stage 4; until then, refuse. + * The geometry GUCs changed. The old ring must still be applied + * first — its batch files follow the recorded geometry — and only + * then can the ring be recreated under the new one. The fresh + * control restarts the generation from zero, which is safe exactly + * because the wipe left no slot behind. */ - ereport(FATAL, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"%s\" was created with dwb_num_batches = %u and dwb_batch_pages = %u", - DWB_DIR, control.num_batches, control.batch_pages), - errhint("Restore the previous settings."))); + if (need_apply) + applied_upto = DWBApplyPass(&control); + ereport(LOG, + (errmsg("recreating double write buffer ring: geometry changed from %u batches of %u pages to %d batches of %d pages", + control.num_batches, control.batch_pages, + dwb_num_batches, dwb_batch_pages))); + DWBWipeRing(); + DWBCreateRing(); + if (!DWBReadControlFile(&control, false)) + pg_unreachable(); } - - /* - * Stage 4: on an unclean start the apply-pass over generation - * control.generation runs here, before the bump. - */ + else if (need_apply) + applied_upto = DWBApplyPass(&control); control.generation++; + control.flags &= ~DWB_CONTROL_RING_CLEAN; control.crc = DWBControlCrc(&control); DWBWriteControlFile(&control); @@ -89,4 +479,62 @@ DWBStartup(void) ereport(LOG, (errmsg("double write buffer ring opened: %d batches of %d pages, generation " UINT64_FORMAT, dwb_num_batches, dwb_batch_pages, control.generation))); + + return applied_upto; +} + +/* + * True when no on-disk slot covers a data write that could still be + * pending: every batch is FREE, or ALLOCATED (an open batch whose staged + * copies live in shared memory only — nothing of it has been written to + * the ring files, and its file still holds the fully retired slots of the + * previous incarnation). + */ +static bool +DWBRingIsQuiescent(void) +{ + for (int i = 0; i < dwb_num_batches; i++) + { + DWBatchState state = DWBGetBatchState(i); + + if (state != DWB_FREE && state != DWB_ALLOCATED) + return false; + } + return true; +} + +/* + * Called at the tail of a clean shutdown, after the shutdown checkpoint or + * restartpoint. Retires whatever the checkpoint left behind (a shutdown + * restartpoint can be skipped entirely, leaving segment fsyncs pending) + * and then sets RING_CLEAN in the ring's control file, entitling the next + * start to skip the apply-pass and legalizing a start under a different + * io_torn_pages_protection mode. If the ring cannot be fully retired the + * marker simply stays unset — the next start applies the ring, which is + * always safe. + */ +void +DWBMarkCleanShutdown(void) +{ + DWBControlFileData control; + + if (!DWBIsEnabled()) + return; + + if (!DWBRingIsQuiescent()) + { + DWBRetireAllSync(); + if (!DWBRingIsQuiescent()) + { + ereport(LOG, + (errmsg("double write buffer ring could not be fully retired; not marking it cleanly shut down"))); + return; + } + } + + if (!DWBReadControlFile(&control, false)) + pg_unreachable(); + control.flags |= DWB_CONTROL_RING_CLEAN; + control.crc = DWBControlCrc(&control); + DWBWriteControlFile(&control); } diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c index 7bb801bb88612..41b1a3137a71b 100644 --- a/src/bin/pg_controldata/pg_controldata.c +++ b/src/bin/pg_controldata/pg_controldata.c @@ -310,6 +310,8 @@ main(int argc, char *argv[]) ControlFile->max_locks_per_xact); printf(_("track_commit_timestamp setting: %s\n"), ControlFile->track_commit_timestamp ? _("on") : _("off")); + printf(_("io_torn_pages_protection setting: %s\n"), + DWBProtectionModeName(ControlFile->io_torn_pages_protection)); printf(_("Maximum data alignment: %u\n"), ControlFile->maxAlign); /* we don't print floatFormat since can't say much useful about it */ diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c index efb515e8a1ac2..31ad3e0686555 100644 --- a/src/bin/pg_resetwal/pg_resetwal.c +++ b/src/bin/pg_resetwal/pg_resetwal.c @@ -708,6 +708,7 @@ GuessControlValues(void) ControlFile.wal_level = WAL_LEVEL_MINIMAL; ControlFile.wal_log_hints = false; ControlFile.track_commit_timestamp = false; + ControlFile.io_torn_pages_protection = DWB_PROTECT_FULL_PAGES; ControlFile.MaxConnections = 100; ControlFile.max_wal_senders = 10; ControlFile.max_worker_processes = 8; @@ -917,6 +918,7 @@ RewriteControlFile(void) ControlFile.wal_level = WAL_LEVEL_MINIMAL; ControlFile.wal_log_hints = false; ControlFile.track_commit_timestamp = false; + ControlFile.io_torn_pages_protection = DWB_PROTECT_FULL_PAGES; ControlFile.MaxConnections = 100; ControlFile.max_wal_senders = 10; ControlFile.max_worker_processes = 8; diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c index 2ca1bcfde7f57..95a31f4387d69 100644 --- a/src/bin/pg_rewind/libpq_source.c +++ b/src/bin/pg_rewind/libpq_source.c @@ -134,21 +134,11 @@ init_libpq_conn(PGconn *conn) /* * Also check that the source server actually writes full page images. We * can get torn pages if a page is modified while we read it with - * pg_read_binary_file(), and we rely on full page images to fix them. The - * full_page_writes GUC alone is not the authority: under - * io_torn_pages_protection = "double_writes" or "off" page images are - * forced off while the GUC may still read "on" (its value only matters - * under "full_pages"). The double write buffer cannot substitute here: - * it repairs torn writes of its own instance, not torn reads of a remote - * copy. Rewinding from a stopped source (--source-pgdata) has no such - * requirement. + * pg_read_binary_file(), and we rely on full page images to fix them. + * This GUC only has its usual meaning under io_torn_pages_protection = + * "full_pages"; the other modes are refused outright based on the + * source's pg_control (see the up-front check in pg_rewind.c). */ - str = run_simple_query(conn, "SHOW io_torn_pages_protection"); - if (strcmp(str, "full_pages") != 0) - pg_fatal("\"io_torn_pages_protection\" must be \"full_pages\" in the source server, not \"%s\"", - str); - pg_free(str); - str = run_simple_query(conn, "SHOW full_page_writes"); if (strcmp(str, "on") != 0) pg_fatal("\"full_page_writes\" must be enabled in the source server"); diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c index 723d6b598736c..bc9795dc1efc6 100644 --- a/src/bin/pg_rewind/pg_rewind.c +++ b/src/bin/pg_rewind/pg_rewind.c @@ -320,6 +320,29 @@ main(int argc, char **argv) else source = init_local_source(datadir_source); + /* + * A live source must itself be protected by full page images: reading + * files from a running server can catch pages mid-write, and only WAL + * page images repair such torn reads on the rewound target. Under + * io_torn_pages_protection = "double_writes" or "off" the source's WAL + * has no images (its double write buffer repairs its own torn writes, not + * our torn reads), so refuse up front, before the target is touched in + * any way. A stopped source has no such requirement. The mode is read + * from the source's pg_control — the authoritative record, unlike the + * legacy full_page_writes GUC, which only matters under "full_pages" and + * is checked in init_libpq_source. + */ + if (connstr_source) + { + buffer = source->fetch_file(source, XLOG_CONTROL_FILE, &size); + digestControlFile(&ControlFile_source, buffer, size); + pg_free(buffer); + + if (ControlFile_source.io_torn_pages_protection != DWB_PROTECT_FULL_PAGES) + pg_fatal("\"io_torn_pages_protection\" must be \"full_pages\" in the source server, not \"%s\"", + DWBProtectionModeName(ControlFile_source.io_torn_pages_protection)); + } + checkTargetDwb(); /* diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h index 2cf8d55d706d1..50d627f2d520e 100644 --- a/src/include/access/xlog_internal.h +++ b/src/include/access/xlog_internal.h @@ -31,7 +31,7 @@ /* * Each page of XLOG file has a header like this: */ -#define XLOG_PAGE_MAGIC 0xD118 /* can be used as WAL version indicator */ +#define XLOG_PAGE_MAGIC 0xD119 /* can be used as WAL version indicator */ typedef struct XLogPageHeaderData { @@ -280,6 +280,7 @@ typedef struct xl_parameter_change int wal_level; bool wal_log_hints; bool track_commit_timestamp; + int io_torn_pages_protection; } xl_parameter_change; /* logs restore point */ diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h index 63e834a6ce477..d98d6fb8081ef 100644 --- a/src/include/catalog/pg_control.h +++ b/src/include/catalog/pg_control.h @@ -22,11 +22,42 @@ /* Version identifier for this pg_control format */ -#define PG_CONTROL_VERSION 1800 +#define PG_CONTROL_VERSION 1801 /* Nonce key length, see below */ #define MOCK_AUTH_NONCE_LEN 32 +/* + * The torn-page protection mechanism (GUC io_torn_pages_protection). Like + * wal_level, the value in force on the WAL-generating server is a protocol + * fact: it decides whether the WAL carries full page images, so it is + * recorded in pg_control and in XLOG_PARAMETER_CHANGE records for replay to + * track. Defined here rather than in storage/dwb.h so that frontend code + * reading pg_control can use it. + */ +typedef enum +{ + DWB_PROTECT_OFF, + DWB_PROTECT_FULL_PAGES, + DWB_PROTECT_DOUBLE_WRITES, +} DWBTornPageProtection; + +/* GUC-spelling name of a DWBTornPageProtection value, for messages */ +static inline const char * +DWBProtectionModeName(int mode) +{ + switch (mode) + { + case DWB_PROTECT_OFF: + return "off"; + case DWB_PROTECT_FULL_PAGES: + return "full_pages"; + case DWB_PROTECT_DOUBLE_WRITES: + return "double_writes"; + } + return "unrecognized"; +} + /* * Body of CheckPoint XLOG records. This is declared here because we keep * a copy of the latest one in pg_control for possible disaster recovery. @@ -183,6 +214,7 @@ typedef struct ControlFileData int max_prepared_xacts; int max_locks_per_xact; bool track_commit_timestamp; + int io_torn_pages_protection; /* DWBTornPageProtection */ /* * This data is used to check for hardware-architecture compatibility of diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index f8d906413dd9a..77cfc0227b7ac 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -23,6 +23,7 @@ #define DWB_H #include "access/xlogdefs.h" +#include "catalog/pg_control.h" #include "port/pg_crc32c.h" #include "storage/buf_internals.h" #include "storage/condition_variable.h" @@ -32,13 +33,10 @@ #include "utils/hsearch.h" #include "utils/timestamp.h" -/* GUC: io_torn_pages_protection */ -typedef enum -{ - DWB_PROTECT_OFF, - DWB_PROTECT_FULL_PAGES, - DWB_PROTECT_DOUBLE_WRITES, -} DWBTornPageProtection; +/* + * The io_torn_pages_protection GUC values (DWBTornPageProtection) live in + * catalog/pg_control.h: the mode is recorded in pg_control. + */ /* GUC: dwb_on_stall (Stage B backpressure behaviour) */ typedef enum @@ -108,11 +106,22 @@ typedef struct DWBControlFileData uint32 min_version; uint32 num_batches; uint32 batch_pages; + uint32 flags; /* DWB_CONTROL_* */ uint64 generation; /* apply-pass horizon: bumped durably on every * start before the ring opens */ pg_crc32c crc; /* CRC of all preceding fields */ } DWBControlFileData; +/* + * DWBControlFileData.flags. RING_CLEAN certifies that every data-file write + * covered by an on-disk slot had been fsynced when the server shut down: it + * is written at the end of a clean shutdown after the ring is fully retired, + * and cleared by the next startup before the ring reopens. While it is set, + * the ring holds no unapplied repairs, so the apply-pass can be skipped and + * a start under a different io_torn_pages_protection mode is legal. + */ +#define DWB_CONTROL_RING_CLEAN 0x0001 + typedef struct DWBBatchHeader { uint32 magic; @@ -378,6 +387,7 @@ extern pg_crc32c DWBControlCrc(const DWBControlFileData *control); extern pg_crc32c DWBBatchHeaderCrc(const DWBBatchHeader *hdr); /* dwb_recovery.c */ -extern void DWBStartup(void); +extern XLogRecPtr DWBStartup(bool unclean_start, bool restoring_backup); +extern void DWBMarkCleanShutdown(void); #endif /* DWB_H */ diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index 6d77cc442c7b0..76a539e4ac494 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -46,6 +46,7 @@ tests += { 't/007_rewind.pl', 't/008_modes.pl', 't/009_fpw_transition.pl', + 't/010_recovery.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/001_dwb.pl b/src/test/modules/test_dwb/t/001_dwb.pl index 3243ba0ea5529..17f2346e54924 100644 --- a/src/test/modules/test_dwb/t/001_dwb.pl +++ b/src/test/modules/test_dwb/t/001_dwb.pl @@ -242,26 +242,34 @@ sub flip_byte $node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=16/, 'ring idle after the stale-open scenario'); -# --- geometry is fixed by the on-disk control file --------------------- +# --- a geometry change recreates the ring ------------------------------ +# The on-disk layout follows the geometry GUCs, so a change rebuilds the +# ring from scratch (after applying the old one if needed — exercised in +# t/010_recovery.pl); the generation restarts with the fresh control file. $node->stop; +my $log_offset = -s $node->logfile; $node->append_conf('postgresql.conf', 'dwb_num_batches = 32'); -my $ret = $node->start(fail_ok => 1); -is($ret, 0, 'start refused after geometry change'); -ok( $node->log_contains('was created with dwb_num_batches = 16'), - 'geometry mismatch reported'); -$node->append_conf('postgresql.conf', 'dwb_num_batches = 16'); $node->start; +ok( $node->log_contains( + 'recreating double write buffer ring: geometry changed from 16 batches of 16 pages to 32 batches of 16 pages', + $log_offset), + 'geometry change recreates the ring'); +ok( $node->log_contains( + qr/ring opened: 32 batches of 16 pages, generation 1\b/, + $log_offset), + 'recreated ring opens with a fresh generation'); $node->stop; - -# the second geometry GUC is enforced independently -my $log_offset = -s $node->logfile; -$node->append_conf('postgresql.conf', 'dwb_batch_pages = 32'); -$ret = $node->start(fail_ok => 1); -is($ret, 0, 'start refused after batch_pages change'); -ok( $node->log_contains('was created with dwb_num_batches = 16 and dwb_batch_pages = 16', +$log_offset = -s $node->logfile; +$node->append_conf('postgresql.conf', + 'dwb_num_batches = 16 +dwb_batch_pages = 32'); +$node->start; +ok( $node->log_contains( + 'geometry changed from 32 batches of 16 pages to 16 batches of 32 pages', $log_offset), - 'batch_pages mismatch reported'); + 'batch_pages change recreates the ring too'); +$node->stop; $node->append_conf('postgresql.conf', 'dwb_batch_pages = 16'); $node->start; $node->stop; @@ -277,7 +285,7 @@ sub flip_byte max_worker_processes = 1 dwb_retire_workers = 1 )); -$ret = $node->start(fail_ok => 1); +my $ret = $node->start(fail_ok => 1); is($ret, 0, 'start refused when the pool does not fit into worker slots'); ok( $node->log_contains( 'needs more "max_worker_processes" slots than remain free', diff --git a/src/test/modules/test_dwb/t/006_backup.pl b/src/test/modules/test_dwb/t/006_backup.pl index ca856374a4b13..cf0b018a5bcab 100644 --- a/src/test/modules/test_dwb/t/006_backup.pl +++ b/src/test/modules/test_dwb/t/006_backup.pl @@ -122,6 +122,51 @@ '100', 'restored data is intact'); $restored->stop; +# --- a ring shipped into a restore by a foreign tool is discarded --------- + +# pg_basebackup excludes the ring, but a third-party backup tool may ship +# pg_dwb/ contents into the restore. Neither staleness defence works +# there — the slots carry the restored control's own generation, and the +# restored data files are legitimately older than the slot copies — so a +# start from a base backup (backup_label present) must not apply the ring: +# it is wiped and recreated cold. The planted control file is garbage, +# which without the guard would be a fatal checksum error. +my $planted = PostgreSQL::Test::Cluster->new('dwb_planted'); +$planted->init_from_backup($node, 'content_check'); +ok(-f $planted->data_dir . '/backup_label', + 'the restore still carries backup_label'); +append_to_file($planted->data_dir . '/pg_dwb/control', 'torn by the tool'); +append_to_file($planted->data_dir . '/pg_dwb/batch_9999', 'foreign slots'); + +my $planted_log_offset = -s $planted->logfile; +$planted->start; +ok( $planted->log_contains( + qr/discarding double write buffer ring contents restored from a base backup/, + $planted_log_offset), + 'the restored ring is discarded'); +ok( !$planted->log_contains( + qr/double write buffer recovery:/, $planted_log_offset), + '... without an apply-pass over it'); +ok( $planted->log_contains( + qr/ring opened: 16 batches of 16 pages, generation 1\b/, + $planted_log_offset), + '... and a fresh ring is created cold'); +ok(!-f $planted->data_dir . '/pg_dwb/batch_9999', + 'the foreign ring files are gone'); +is( $planted->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpi'), + '100', 'restored data is intact'); + +# once the backup recovery is over the guard is gone: an ordinary crash +# of this cluster is served by the apply-pass again +$planted->stop('immediate'); +$planted_log_offset = -s $planted->logfile; +$planted->start; +ok( $planted->log_contains( + qr/double write buffer recovery: \d+ of \d+ candidate pages restored/, + $planted_log_offset), + 'a later crash of the restored cluster applies the ring normally'); +$planted->stop; + # --- pg_dwb as a symlink backs up as an empty real directory -------------- SKIP: diff --git a/src/test/modules/test_dwb/t/007_rewind.pl b/src/test/modules/test_dwb/t/007_rewind.pl index 296985f2c1810..cd23c50a67671 100644 --- a/src/test/modules/test_dwb/t/007_rewind.pl +++ b/src/test/modules/test_dwb/t/007_rewind.pl @@ -163,10 +163,10 @@ rename($stash, $dwb_path) or BAIL_OUT("could not restore $dwb_path: $!"); } -# return the target to a clean shutdown for the rewind proper; recovery -# runs with the ring already behind the symlink -$node_a->start; -$node_a->stop('fast'); +# The target stays crashed on purpose: the rewind itself drives it to a +# clean shutdown through ensureCleanShutdown's single-user run, which +# exercises the apply-pass and the ring drain with no worker pool at all — +# and with the ring already behind the symlink. # --- a broken ring symlink on the source is harmless --------------------- diff --git a/src/test/modules/test_dwb/t/008_modes.pl b/src/test/modules/test_dwb/t/008_modes.pl index 9fedf15e13677..868153190afa8 100644 --- a/src/test/modules/test_dwb/t/008_modes.pl +++ b/src/test/modules/test_dwb/t/008_modes.pl @@ -98,4 +98,40 @@ sub wal_window unlike($out, qr/FPW_CHANGE/, 'the no-op reload emitted no XLOG_FPW_CHANGE record'); +# --- leaving double_writes takes one clean shutdown ---------------------- + +# After a crash the ring may hold repairs only a double_writes start can +# apply, so a start in any other mode is refused until the ring has been +# closed cleanly once. +$node->stop('immediate'); + +$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +my $ret = $node->start(fail_ok => 1); +is($ret, 0, 'crashed ring refuses a full_pages start'); +ok( $node->log_contains( + qr/FATAL: .* the double write buffer ring was not cleanly shut down, cannot start with "io_torn_pages_protection=full_pages"/ + ), + '... naming the mode change as the problem'); + +$node->append_conf('postgresql.conf', 'io_torn_pages_protection = off'); +$ret = $node->start(fail_ok => 1); +is($ret, 0, 'crashed ring refuses an off start too'); + +# one double_writes start applies the ring, and a clean stop releases it +$node->append_conf('postgresql.conf', + 'io_torn_pages_protection = double_writes'); +$log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains(qr/double write buffer recovery:/, $log_offset), + 'the double_writes start runs the apply-pass'); +$node->stop; + +$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +$log_offset = -s $node->logfile; +$node->start; +is( $node->safe_psql('postgres', 'SHOW io_torn_pages_protection'), + 'full_pages', 'after a clean stop the mode change is legal'); +ok( !$node->log_contains(qr/ring opened/, $log_offset), + '... and the leftover ring stays closed'); + done_testing(); diff --git a/src/test/modules/test_dwb/t/009_fpw_transition.pl b/src/test/modules/test_dwb/t/009_fpw_transition.pl index 78befa7eaaf96..225cb33677ff3 100644 --- a/src/test/modules/test_dwb/t/009_fpw_transition.pl +++ b/src/test/modules/test_dwb/t/009_fpw_transition.pl @@ -1,20 +1,24 @@ # Copyright (c) 2025, PostgreSQL Global Development Group -# A primary restarted into io_torn_pages_protection = double_writes emits -# no XLOG_FPW_CHANGE (UpdateFullPageWrites at startup runs before recovery -# is marked done, and the checkpointer's later call sees no remaining -# change), so the checkpoints written after the restart are the only -# replayed evidence that page images stopped. Crossing that transition -# must fail pg_backup_stop() for an online backup opened on a standby, -# and a standby without a double write buffer of its own must warn once -# per startup. +# Two ways a primary can stop writing page images, and how a full_pages +# standby reacts to each. Disabling the legacy full_page_writes GUC (the +# primary staying in full_pages mode) is the vanilla situation: the standby +# keeps replaying, but an online backup opened on it cannot be closed. The +# transition is carried by checkpoint records only — a restart emits no +# XLOG_FPW_CHANGE (UpdateFullPageWrites at startup runs before recovery is +# marked done, and the checkpointer's later call sees no remaining change). +# Switching the primary to io_torn_pages_protection = double_writes is a +# protocol change recorded in pg_control and XLOG_PARAMETER_CHANGE, and a +# standby still expecting full-page protection is refused outright: its own +# crash would leave torn pages nothing can repair. use strict; use warnings FATAL => 'all'; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; +use Time::HiRes qw(usleep); my $primary = PostgreSQL::Test::Cluster->new('dwb_fpw_primary'); $primary->init(allows_streaming => 1); @@ -41,28 +45,17 @@ my $backer = $standby->background_psql('postgres', on_error_stop => 0); $backer->query_safe("SELECT pg_backup_start('dwb_fpw_transition')"); -# --- the primary crosses into double_writes via a restart ---------------- +# --- the legacy GUC is disabled under full_pages ------------------------- -my $warn_offset = -s $standby->logfile; -$primary->append_conf('postgresql.conf', - 'io_torn_pages_protection = double_writes'); +$primary->append_conf('postgresql.conf', 'full_page_writes = off'); $primary->restart; $primary->safe_psql('postgres', 'INSERT INTO dwb_fpw SELECT g FROM generate_series(1001, 2000) g'); -$primary->safe_psql('postgres', 'CHECKPOINT'); -# a second image-less checkpoint proves the warning below does not repeat +# the post-restart checkpoint records are the only replayed evidence $primary->safe_psql('postgres', 'CHECKPOINT'); $primary->wait_for_catchup($standby); -# --- the DWB-less standby warns exactly once ----------------------------- - -my $log = slurp_file($standby->logfile, $warn_offset); -my @warnings = $log =~ - /(replaying WAL generated without full page images, but this server does not use the double write buffer)/g; -is(scalar(@warnings), 1, - 'standby without a ring of its own warns exactly once per startup'); - # --- the open backup cannot be closed cleanly ---------------------------- my $stop_offset = -s $standby->logfile; @@ -72,6 +65,61 @@ qr/WAL generated without full page images was replayed during online backup/, $stop_offset), '... naming the replayed image-less WAL'); +ok( $standby->log_contains( + qr/the primary has "full_page_writes" disabled/, $stop_offset), + '... and blaming the legacy GUC, not the mode'); $backer->quit; +# --- the primary switches to double_writes ------------------------------- + +# The standby still runs full_pages: replaying the XLOG_PARAMETER_CHANGE +# that announces the mode must be fatal, and the whole standby exits. +my $fatal_offset = -s $standby->logfile; +$primary->append_conf('postgresql.conf', + 'io_torn_pages_protection = double_writes'); +$primary->restart; +$primary->safe_psql('postgres', + 'INSERT INTO dwb_fpw SELECT g FROM generate_series(2001, 3000) g'); + +foreach my $i (1 .. 300) +{ + last unless -f $standby->data_dir . '/postmaster.pid'; + usleep(100_000); +} +ok(!-f $standby->data_dir . '/postmaster.pid', + 'full_pages standby dies replaying the double_writes transition'); +# the node died on its own; let the harness notice before restarting it +$standby->stop('fast', fail_ok => 1); +ok( $standby->log_contains( + qr/FATAL: .* WAL was generated with "io_torn_pages_protection=double_writes", cannot continue recovering with "io_torn_pages_protection=full_pages"/, + $fatal_offset), + '... with the incompatibility spelled out'); + +# The refusal is durable: the mode is in the standby's pg_control now, so a +# restart is refused up front, before any replay. +$fatal_offset = -s $standby->logfile; +my $ret = $standby->start(fail_ok => 1); +is($ret, 0, 'restarting the full_pages standby is refused up front'); +ok( $standby->log_contains( + qr/WAL was generated with "io_torn_pages_protection=double_writes"/, + $fatal_offset), + '... for the same reason'); + +# --- a double_writes standby follows the same primary -------------------- + +$standby->append_conf('postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +)); +my $ring_offset = -s $standby->logfile; +$standby->start; +ok( $standby->log_contains( + qr/double write buffer ring opened: 16 batches of 16 pages, generation 1\b/, + $ring_offset), + 'reconfigured standby cold-starts a ring of its own'); +$primary->wait_for_catchup($standby); +is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpw'), + '3000', 'and replays the image-less WAL'); + done_testing(); diff --git a/src/test/modules/test_dwb/t/010_recovery.pl b/src/test/modules/test_dwb/t/010_recovery.pl new file mode 100644 index 0000000000000..52ab1f7a69d1a --- /dev/null +++ b/src/test/modules/test_dwb/t/010_recovery.pl @@ -0,0 +1,206 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# The startup apply-pass: torn data pages are repaired from the ring before +# WAL replay. The scenarios pick pages replay itself can never fix — a +# hint-bit-only page is logged as XLOG_FPI_FOR_HINT without an image and +# redo does not even read it, and the other damaged pages predate the last +# checkpoint — so any repair observed here came from the ring. Also pins +# the boundaries of the pass: a second pass over the same ring is a no-op +# (idempotence after a crash mid-pass), a clean start skips the pass, and a +# slot of a past generation is never applied, even to a page that fails +# verification. +# +# With dwb_retire_workers = 0 every publish retires its batch on the spot, +# so a sequential writer keeps reusing the lowest ring index and only the +# LAST page written before a crash still has its slot on disk. Each damage +# scenario therefore ends its run with the target page's flush: +# BufferSync sorts a checkpoint's writes by relfilenode, and the user +# tables here sort after every catalog page a session may have hint-dirtied. + +use strict; +use warnings FATAL => 'all'; +use File::Path qw(rmtree); +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::RecursiveCopy; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_recovery'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 0 +dwb_batch_timeout_ms = 20 +autovacuum = off +bgwriter_lru_maxpages = 0 +log_min_messages = debug1 +)); +$node->start; + +sub read_block +{ + my ($file, $blkno) = @_; + my $buf; + + open my $fh, '<:raw', $file or die "could not open $file: $!"; + sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; + sysread($fh, $buf, 8192) == 8192 or die "short read from $file: $!"; + close $fh; + return $buf; +} + +sub write_block +{ + my ($file, $blkno, $buf) = @_; + + open my $fh, '+<:raw', $file or die "could not open $file: $!"; + sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; + syswrite($fh, $buf) == length($buf) or die "short write to $file: $!"; + close $fh; + return; +} + +$node->safe_psql('postgres', q( + CREATE TABLE thint AS SELECT g AS id FROM generate_series(1, 100) g; + CREATE TABLE told AS SELECT g AS id FROM generate_series(1, 100) g; +)); +$node->safe_psql('postgres', 'CHECKPOINT'); + +my $thint_file = + $node->data_dir . '/' + . $node->safe_psql('postgres', "SELECT pg_relation_filepath('thint')"); +my $told_file = + $node->data_dir . '/' + . $node->safe_psql('postgres', "SELECT pg_relation_filepath('told')"); +my $thint_relnum = $node->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE relname = 'thint'"); +my $told_relnum = $node->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE relname = 'told'"); + +# --- a torn hint-bit-only page is repaired before replay ----------------- + +# With checksums on and page images off, setting hint bits logs +# XLOG_FPI_FOR_HINT without an image and still advances the page LSN, and +# redo of such a record never reads the page — the original silent-loss +# hole. Dirty thint's page with hint bits only, flush it through the ring, +# crash, and tear the on-disk page as if that flush had been cut short. +$node->safe_psql('postgres', 'SELECT count(*) FROM thint'); +$node->safe_psql('postgres', 'CHECKPOINT'); +$node->stop('immediate'); + +write_block($thint_file, 0, + substr(read_block($thint_file, 0), 0, 4096) . ("\0" x 4096)); + +my $log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains( + qr/double write buffer recovery: 1 of 1 candidate pages restored/, + $log_offset), + 'apply-pass restored the torn page'); +ok( $node->log_contains( + qr!restoring page 0 of relation \d+/\d+/$thint_relnum fork 0!, + $log_offset), + '... and it was the hint-bit page'); +ok( $node->log_contains( + qr/ring opened: 16 batches of 16 pages, generation 2\b/, $log_offset), + 'generation bumped after the pass'); +is( $node->safe_psql('postgres', 'SELECT count(*) FROM thint'), + '100', 'torn hint page is whole again'); + +# --- a checksum-valid but stale page is repaired by its LSN -------------- + +# Put the pre-update page image back after the crash: bytewise it verifies +# fine, so only the LSN comparison can see that the ring copy is newer. +my $told_v1 = read_block($told_file, 0); +$node->safe_psql('postgres', 'UPDATE told SET id = id + 1000 WHERE id <= 50'); +$node->safe_psql('postgres', 'CHECKPOINT'); +$node->stop('immediate'); + +write_block($told_file, 0, $told_v1); + +# keep a copy of the whole ring for the re-apply scenario below +my $ring_stash = $node->basedir . '/ring_stash'; +PostgreSQL::Test::RecursiveCopy::copypath($node->data_dir . '/pg_dwb', + $ring_stash); + +$log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains( + qr/double write buffer recovery: 1 of 1 candidate pages restored/, + $log_offset), + 'apply-pass restored the stale page'); +ok( $node->log_contains( + qr!restoring page 0 of relation \d+/\d+/$told_relnum fork 0!, + $log_offset), + '... by its LSN — the page verified fine'); +is( $node->safe_psql( + 'postgres', 'SELECT count(*) FROM told WHERE id > 1000'), + '50', 'stale page carries the update again'); + +# --- a repeated pass over the same ring is a no-op ----------------------- + +# Equivalent to a crash in the middle of the pass: the ring is untouched +# and the generation not yet bumped, so the next start sees the very same +# candidate — now against a repaired, newer-or-equal page. +$node->stop('immediate'); +rmtree($node->data_dir . '/pg_dwb'); +PostgreSQL::Test::RecursiveCopy::copypath($ring_stash, + $node->data_dir . '/pg_dwb'); + +$log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains( + qr/double write buffer recovery: 0 of 1 candidate pages restored/, + $log_offset), + 're-applied pass sees the same candidate and rewrites nothing'); +is( $node->safe_psql( + 'postgres', 'SELECT count(*) FROM told WHERE id > 1000'), + '50', 'data intact after the repeated pass'); + +# --- a clean start skips the pass, but still bumps the generation -------- + +$node->safe_psql('postgres', q( + CREATE TABLE tstale AS SELECT g AS id FROM generate_series(1, 100) g; +)); +$node->safe_psql('postgres', 'CHECKPOINT'); +my $tstale_file = + $node->data_dir . '/' + . $node->safe_psql('postgres', "SELECT pg_relation_filepath('tstale')"); +$node->stop; + +my $tstale_good = read_block($tstale_file, 0); + +$log_offset = -s $node->logfile; +$node->start; +ok( !$node->log_contains(qr/double write buffer recovery:/, $log_offset), + 'clean start runs no apply-pass'); +ok( $node->log_contains(qr/ring opened: .* generation 4\b/, $log_offset), + '... yet the generation still moves, expiring the old slots'); + +# --- a slot of a past generation is never applied ------------------------ + +# tstale's only slot is one generation behind now. Corrupt its page so +# that it fails verification — the branch where an LSN comparison cannot +# veto a repair — and crash: the pass must leave the page alone anyway. +$node->stop('immediate'); +write_block($tstale_file, 0, chr(0xAB) x 8192); + +$log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains( + qr/double write buffer recovery: 0 of 0 candidate pages restored/, + $log_offset), + 'no current-generation candidates after the idle crash'); +is( read_block($tstale_file, 0), chr(0xAB) x 8192, + 'the stale slot was not applied to the corrupted page'); + +# put the good page back so the cluster winds down healthy +write_block($tstale_file, 0, $tstale_good); +is( $node->safe_psql('postgres', 'SELECT count(*) FROM tstale'), + '100', 'page manually restored, cluster consistent'); + +done_testing(); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 279ff418d6c3e..9dfd3beda80d8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -600,6 +600,8 @@ DR_sqlfunction DR_transientrel DSMRegistryCtxStruct DSMRegistryEntry +DWBAppliedFork +DWBApplyCandidate DWBBatchHeader DWBControlFileData DWBOnStall From fd9942705c1a373937f26352254e58319dff9475 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 25 Jul 2026 23:51:36 +0300 Subject: [PATCH 14/52] Harden startup recovery after the Stage 4 review round (Stage 4 follow-up) Key the apply-pass decision on the RING_CLEAN marker alone and drop the unclean_start parameter of DWBStartup. The marker is the exact certificate: it is set only after full retirement and cleared before the ring reopens, while pg_control diverges from the ring in both directions. Keying on pg_control let a crash under an interim full_pages/off run (which touches neither the marker nor the generation) re-arm a fully retired ring: its slots still matched the current generation, and the pass would resurrect ancient page images over blocks torn long after the ring was closed, turning a loud checksum failure into silent corruption. Close the remaining error-path holes around the pass: - Fail closed when backup_label cannot be probed: any errno other than ENOENT is now FATAL instead of reading as "not restoring a backup", which would have applied a ring shipped inside a base backup. - Skip candidates whose on-disk page is new (empty header). A zeroed header proves the covered write's first sector never landed on a block that never held an initialized page, so replay recreates it; repairing would also let a pre-truncate slot image resurrect on a re-extended zero page whose LSN 0 loses the staleness comparison. - Make DWBWipeRing remove the control file first, durably, and let the cold-create path sweep leftovers: a crash mid-wipe used to leave a readable control beside missing batch files, and every retry then died on the ENOENT until manual intervention. - Re-verify the slot image CRC after the repair loop's second read; divergent reads mean failing storage and abort startup instead of writing unverified bytes over a data page. - Cover DB_SHUTDOWNED_IN_RECOVERY in the minRecoveryPoint raise: the pass also runs on a cleanly stopped standby with an unretired ring. - Refuse an unreadable pg_dwb/control in full_pages/off modes with the downgrade guard's message and removal hint instead of a bare low-level FATAL that named no way out. - Warn when crash recovery replays WAL generated under io_torn_pages_protection=off with a different local mode: pages torn by that crash cannot be repaired by any local mechanism. Place io_torn_pages_protection next to wal_level in ControlFileData and xl_parameter_change (both formats were already bumped on this branch), deduplicate applied forks with a hash table, reuse DWBBatchFilePath instead of a second spelling of the batch path, log the ring wipe in non-DWB restore starts, use ereport(LOG) for the minRecoveryPoint raise, and pin down invariants at their definition sites: the enum values are on-disk facts and must not be renumbered, batch_id monotonicity within a run is what makes the equal-LSN tie-break valid, and the flags field fills a former CRC-covered padding hole, which is why version-1 control files read back compatibly. New and extended tests: t/011_geometry_recovery.pl pins that a crashed ring is applied with its recorded geometry before being recreated under new GUCs; t/010 pins the marker-only trigger (clean pg_control, unclean ring) and the dropped-relation skip; t/008 pins that reopening the ring re-arms the downgrade guard and that a corrupt control refuses non-ring modes with the hinted recipe actually working; t/006 pins the planted ring wipe under full_pages; t/005 pins a torn standby page repaired from the standby's own ring. --- src/backend/access/transam/xlog.c | 74 ++++-- src/backend/storage/dwb/dwb_file.c | 44 +++- src/backend/storage/dwb/dwb_recovery.c | 215 ++++++++++++------ src/backend/utils/misc/guc_tables.c | 1 + src/bin/pg_controldata/pg_controldata.c | 4 +- src/bin/pg_rewind/pg_rewind.c | 4 +- src/include/access/xlog_internal.h | 2 +- src/include/catalog/pg_control.h | 25 +- src/include/storage/dwb.h | 31 ++- src/test/modules/test_dwb/meson.build | 1 + src/test/modules/test_dwb/t/001_dwb.pl | 3 +- src/test/modules/test_dwb/t/005_standby.pl | 59 +++++ src/test/modules/test_dwb/t/006_backup.pl | 33 ++- src/test/modules/test_dwb/t/008_modes.pl | 48 ++++ src/test/modules/test_dwb/t/010_recovery.pl | 57 +++++ .../test_dwb/t/011_geometry_recovery.pl | 94 ++++++++ src/test/modules/test_dwb/test_dwb.c | 2 +- 17 files changed, 583 insertions(+), 114 deletions(-) create mode 100644 src/test/modules/test_dwb/t/011_geometry_recovery.pl diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 73c1ba2bbc40e..b03ebe6f5b0bb 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -5630,28 +5630,60 @@ StartupXLOG(void) bool restoring_backup; XLogRecPtr dwb_applied_upto; - restoring_backup = - !XLogRecPtrIsInvalid(ControlFile->backupStartPoint) || - access(BACKUP_LABEL_FILE, F_OK) == 0; + /* + * A failure to probe for backup_label must fail closed: reading its + * absence out of an EACCES/EIO would drop the one guard that keeps a + * ring shipped inside a base backup from being applied into the + * restored cluster. (read_backup_label treats a failing open the + * same way.) + */ + if (access(BACKUP_LABEL_FILE, F_OK) == 0) + restoring_backup = true; + else if (errno != ENOENT) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not access file \"%s\": %m", + BACKUP_LABEL_FILE))); + else + restoring_backup = + !XLogRecPtrIsInvalid(ControlFile->backupStartPoint); - dwb_applied_upto = DWBStartup(didCrash, restoring_backup); + /* + * Crash recovery over WAL generated without any torn page protection + * cannot repair pages the crash tore, whatever the local mode says + * now. The mode-based FATAL in CheckRequiredParameterValues covers + * archive recovery only, so this is the one transition that would + * otherwise be silent. + */ + if (didCrash && + ControlFile->io_torn_pages_protection == DWB_PROTECT_OFF && + io_torn_pages_protection != DWB_PROTECT_OFF) + ereport(WARNING, + (errmsg("database system was interrupted while torn page protection was disabled"), + errdetail("WAL generated with \"io_torn_pages_protection=off\" carries no full page images; pages torn by the crash cannot be repaired by this recovery."))); + + dwb_applied_upto = DWBStartup(restoring_backup); /* - * On a crashed standby, consistency must not be declared before the - * local WAL covers the repaired pages. The write path guarantees - * minRecoveryPoint already does — FlushBuffer's XLogFlush advances - * it durably before the page can enter the ring — so this raise is - * expected to be a no-op; it stays as a belt-and-braces enforcement - * of the invariant. The timeline is left alone: any LSN the ring can - * hold lies on a timeline minRecoveryPoint has already seen, by the - * same write-path argument. + * On a standby that did not durably retire the ring — whether it + * crashed or merely skipped its shutdown restartpoint — consistency + * must not be declared before the local WAL covers the repaired + * pages. The write path guarantees minRecoveryPoint already does — + * FlushBuffer's XLogFlush advances it durably before the page can + * enter the ring — so this raise is expected to be a no-op; it + * stays as a belt-and-braces enforcement of the invariant. The + * timeline is left alone: any LSN the ring can hold lies on a + * timeline minRecoveryPoint has already seen, by the same write-path + * argument. */ - if (ControlFile->state == DB_IN_ARCHIVE_RECOVERY && + if ((ControlFile->state == DB_IN_ARCHIVE_RECOVERY || + ControlFile->state == DB_SHUTDOWNED_IN_RECOVERY) && !XLogRecPtrIsInvalid(ControlFile->minRecoveryPoint) && dwb_applied_upto > ControlFile->minRecoveryPoint) { - elog(LOG, "raising minimum recovery point to %X/%X to cover pages repaired from the double write buffer", - LSN_FORMAT_ARGS(dwb_applied_upto)); + ereport(LOG, + (errmsg("raising minimum recovery point to %X/%X to cover pages repaired from the double write buffer", + LSN_FORMAT_ARGS(dwb_applied_upto)))); LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); ControlFile->minRecoveryPoint = dwb_applied_upto; UpdateControlFile(); @@ -8239,6 +8271,10 @@ XLogReportParameters(void) * with wal_level=minimal anyway. We don't really care about the * values in pg_control either if wal_level=minimal, but seems better * to keep them up-to-date to avoid confusion. + * + * An io_torn_pages_protection change is always WAL-logged: replay + * must learn the generating server's mode, because the FATAL in + * CheckRequiredParameterValues keys on it. */ if (wal_level != ControlFile->wal_level || io_torn_pages_protection != ControlFile->io_torn_pages_protection || @@ -8376,9 +8412,11 @@ UpdateFullPageWrites(void) * off) emits no XLOG_FPW_CHANGE — its checkpoints are the only replayed * evidence of the change (see UpdateFullPageWrites). * - * A server that replays image-less WAL while expecting full-page protection - * is refused outright by CheckRequiredParameterValues, keyed on the - * generating server's io_torn_pages_protection in pg_control. + * Only the mode-based loss of images is refused by + * CheckRequiredParameterValues (keyed on the generating server's + * io_torn_pages_protection in pg_control); the legacy case — a "full_pages" + * primary running with full_page_writes = off — still replays, and this + * tracking is what lets the backup guards reject it. */ static void XLogTrackFullPageWritesDisabled(XLogReaderState *record, bool fpw) diff --git a/src/backend/storage/dwb/dwb_file.c b/src/backend/storage/dwb/dwb_file.c index 0b3b66d5eea78..a08ba911e2f08 100644 --- a/src/backend/storage/dwb/dwb_file.c +++ b/src/backend/storage/dwb/dwb_file.c @@ -81,7 +81,7 @@ DWBBatchHeaderCrc(const DWBBatchHeader *hdr) return crc; } -static void +void DWBBatchFilePath(char *path, int batch_idx) { snprintf(path, MAXPGPATH, DWB_DIR "/batch_%04d", batch_idx); @@ -91,18 +91,31 @@ DWBBatchFilePath(char *path, int batch_idx) * Read pg_dwb/control. Returns false if the file does not exist and * missing_ok; any other failure (including a CRC mismatch) is FATAL — * a damaged control file must not silently degrade the apply-pass. + * + * A caller that can refuse startup with a more helpful message than the + * low-level FATALs may pass corruptp: any failure other than a tolerated + * ENOENT then sets *corruptp and returns false instead. */ bool -DWBReadControlFile(DWBControlFileData *control, bool missing_ok) +DWBReadControlFile(DWBControlFileData *control, bool missing_ok, + bool *corruptp) { int fd; int r; + if (corruptp) + *corruptp = false; + fd = OpenTransientFile(DWB_CONTROL_FILE, O_RDONLY | PG_BINARY); if (fd < 0) { if (errno == ENOENT && missing_ok) return false; + if (corruptp) + { + *corruptp = true; + return false; + } ereport(FATAL, (errcode_for_file_access(), errmsg("could not open file \"%s\": %m", DWB_CONTROL_FILE))); @@ -114,6 +127,12 @@ DWBReadControlFile(DWBControlFileData *control, bool missing_ok) pgstat_report_wait_end(); if (r != sizeof(DWBControlFileData)) { + if (corruptp) + { + *corruptp = true; + CloseTransientFile(fd); + return false; + } /* distinguish a real read error from a truncated file */ if (r < 0) ereport(FATAL, @@ -131,16 +150,25 @@ DWBReadControlFile(DWBControlFileData *control, bool missing_ok) errmsg("could not close file \"%s\": %m", DWB_CONTROL_FILE))); if (control->magic != DWB_CONTROL_MAGIC || - !EQ_CRC32C(control->crc, DWBControlCrc(control))) - ereport(FATAL, - (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("invalid checksum or magic number in file \"%s\"", - DWB_CONTROL_FILE))); - if (control->min_version > DWB_VERSION) + !EQ_CRC32C(control->crc, DWBControlCrc(control)) || + control->min_version > DWB_VERSION) + { + if (corruptp) + { + *corruptp = true; + return false; + } + if (control->magic != DWB_CONTROL_MAGIC || + !EQ_CRC32C(control->crc, DWBControlCrc(control))) + ereport(FATAL, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("invalid checksum or magic number in file \"%s\"", + DWB_CONTROL_FILE))); ereport(FATAL, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("file \"%s\" requires format version at least %u, but this server supports %u", DWB_CONTROL_FILE, control->min_version, DWB_VERSION))); + } return true; } diff --git a/src/backend/storage/dwb/dwb_recovery.c b/src/backend/storage/dwb/dwb_recovery.c index e86712b8cce82..1a597a04c9298 100644 --- a/src/backend/storage/dwb/dwb_recovery.c +++ b/src/backend/storage/dwb/dwb_recovery.c @@ -5,11 +5,12 @@ * the apply-pass that repairs torn data pages, and the durable * generation protocol around it. * - * On every start (clean, unclean or cold) the durable generation in - * pg_dwb/control is bumped BEFORE the ring opens for new writes, so slots - * left behind by the previous run can never masquerade as current after a - * future crash. Order: read G -> (ring not cleanly closed) apply-pass over - * generation G + fsync -> durable control.generation := G+1 -> open ring. + * On every double_writes start (clean, unclean or cold) the durable + * generation in pg_dwb/control is bumped BEFORE the ring opens for new + * writes, so slots left behind by the previous run can never masquerade as + * current after a future crash. Order: read G -> (RING_CLEAN not set) + * apply-pass over generation G + fsync -> durable control.generation := G+1 + * -> open ring. Starts in the other modes leave the ring untouched. * * The apply-pass runs before WAL replay and repairs the data files * directly: a candidate slot must carry a valid meta_crc, the current @@ -48,18 +49,27 @@ typedef struct DWBApplyCandidate { BufferTag tag; /* hash key */ XLogRecPtr lsn; - uint64 batch_id; /* tie-breaker for equal LSNs */ + uint64 batch_id; /* tie-breaker for equal LSNs: equal + * generation means one server run, where + * batch_id is monotonic in publication order + * (see DWBBatchHeader.batch_id), so the + * higher id holds the later copy */ uint32 batch_idx; uint32 slot_idx; + pg_crc32c image_crc; /* revalidates the image on re-read */ } DWBApplyCandidate; -/* one fork the apply-pass has written to and must fsync */ +/* one fork the apply-pass has written to and must fsync (HASH_BLOBS key) */ typedef struct DWBAppliedFork { RelFileLocator rlocator; ForkNumber forknum; } DWBAppliedFork; +StaticAssertDecl(sizeof(DWBAppliedFork) == + sizeof(RelFileLocator) + sizeof(ForkNumber), + "DWBAppliedFork has padding; unsafe as a HASH_BLOBS key"); + static XLogRecPtr DWBApplyPass(const DWBControlFileData *control); static void DWBWipeRing(void); static bool DWBRingIsQuiescent(void); @@ -114,10 +124,10 @@ DWBApplyPass(const DWBControlFileData *control) char *disk_buf = palloc_aligned(BLCKSZ, PG_IO_ALIGN_SIZE, 0); HASHCTL info; HTAB *candidates; + HTAB *applied_forks; HASH_SEQ_STATUS seq; DWBApplyCandidate *cand; - DWBAppliedFork *applied_forks; - int n_applied_forks = 0; + DWBAppliedFork *fork; int n_candidates = 0; int n_applied = 0; XLogRecPtr applied_upto = InvalidXLogRecPtr; @@ -129,6 +139,12 @@ DWBApplyPass(const DWBControlFileData *control) &info, HASH_ELEM | HASH_BLOBS); + /* forks written to, for the final fsync sweep */ + info.keysize = sizeof(DWBAppliedFork); + info.entrysize = sizeof(DWBAppliedFork); + applied_forks = hash_create("DWB apply-pass forks", 16, &info, + HASH_ELEM | HASH_BLOBS); + /* * Scan every batch file with the geometry recorded in control. A batch * or slot that fails any local validity check is skipped, not an error: @@ -144,7 +160,7 @@ DWBApplyPass(const DWBControlFileData *control) DWBBatchHeader hdr; DWSlotMeta *metas; - snprintf(path, sizeof(path), DWB_DIR "/batch_%04u", batch_idx); + DWBBatchFilePath(path, batch_idx); fd = OpenTransientFile(path, O_RDONLY | PG_BINARY); if (fd < 0) ereport(ERROR, @@ -196,6 +212,13 @@ DWBApplyPass(const DWBControlFileData *control) if (!EQ_CRC32C(meta->image_crc, DWBImageCrc(image_buf))) continue; + /* + * Keep the highest LSN; on equal LSNs the later batch wins (equal + * LSNs with different contents are real: a re-flush after + * hint-bit-only changes does not move the LSN). Within one batch + * the later slot wins by plain overwrite, matching the order the + * slots were filled in. + */ n_candidates++; entry = hash_search(candidates, &meta->tag, HASH_ENTER, &found); if (found && @@ -207,6 +230,7 @@ DWBApplyPass(const DWBControlFileData *control) entry->batch_id = hdr.batch_id; entry->batch_idx = batch_idx; entry->slot_idx = slot_idx; + entry->image_crc = meta->image_crc; } if (CloseTransientFile(fd) != 0) @@ -222,9 +246,6 @@ DWBApplyPass(const DWBControlFileData *control) * skipped: there is nothing to repair and replay or a replayed truncate * drives the final state. */ - applied_forks = palloc(sizeof(DWBAppliedFork) * - hash_get_num_entries(candidates)); - hash_seq_init(&seq, candidates); while ((cand = hash_seq_search(&seq)) != NULL) { @@ -234,7 +255,7 @@ DWBApplyPass(const DWBControlFileData *control) SMgrRelation reln; char path[MAXPGPATH]; int fd; - bool known_fork; + DWBAppliedFork fkey; reln = smgropen(rlocator, INVALID_PROC_NUMBER); if (!smgrexists(reln, forknum)) @@ -243,12 +264,29 @@ DWBApplyPass(const DWBControlFileData *control) continue; smgrread(reln, forknum, blkno, disk_buf); + + /* + * A "new" page (empty header) is never repaired. Every staged image + * is an initialized page, so an empty on-disk header means the + * covered write's first sector never reached disk and the block had + * never held an initialized page before — its init record therefore + * lies after the last checkpoint, and replay recreates the page + * without reading the current contents. The skip is also required + * for correctness in the other direction: after a truncate + + * re-extend within one generation the ring can hold a pre-truncate + * copy of this block, and the re-extended zeroed page (LSN 0) would + * lose the LSN comparison below to that stale image, which nothing + * would then replay over. + */ + if (PageIsNew((Page) disk_buf)) + continue; + if (PageIsVerified((Page) disk_buf, blkno, PIV_LOG_LOG, NULL) && PageGetLSN((Page) disk_buf) >= cand->lsn) continue; /* re-read the winning copy; the scan buffer is long overwritten */ - snprintf(path, sizeof(path), DWB_DIR "/batch_%04u", cand->batch_idx); + DWBBatchFilePath(path, (int) cand->batch_idx); fd = OpenTransientFile(path, O_RDONLY | PG_BINARY); if (fd < 0) ereport(ERROR, @@ -261,6 +299,18 @@ DWBApplyPass(const DWBControlFileData *control) (errcode_for_file_access(), errmsg("could not close file \"%s\": %m", path))); + /* + * The image was CRC-checked during the scan, but this is a second + * physical read; a divergence means the storage returned different + * bytes twice, and writing them over a data page would defeat the + * pass's whole purpose. + */ + if (!EQ_CRC32C(cand->image_crc, DWBImageCrc(image_buf))) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("page image in file \"%s\" slot %u failed verification on re-read", + path, cand->slot_idx))); + elog(DEBUG1, "double write buffer recovery: restoring page %u of relation %u/%u/%u fork %d from batch %u slot %u (LSN %X/%X)", blkno, rlocator.spcOid, rlocator.dbOid, rlocator.relNumber, forknum, cand->batch_idx, cand->slot_idx, @@ -272,36 +322,23 @@ DWBApplyPass(const DWBControlFileData *control) if (cand->lsn > applied_upto) applied_upto = cand->lsn; - known_fork = false; - for (int i = 0; i < n_applied_forks; i++) - { - if (RelFileLocatorEquals(applied_forks[i].rlocator, rlocator) && - applied_forks[i].forknum == forknum) - { - known_fork = true; - break; - } - } - if (!known_fork) - { - applied_forks[n_applied_forks].rlocator = rlocator; - applied_forks[n_applied_forks].forknum = forknum; - n_applied_forks++; - } + fkey.rlocator = rlocator; + fkey.forknum = forknum; + (void) hash_search(applied_forks, &fkey, HASH_ENTER, NULL); } /* make the repairs durable before the generation moves on */ - for (int i = 0; i < n_applied_forks; i++) - smgrimmedsync(smgropen(applied_forks[i].rlocator, - INVALID_PROC_NUMBER), - applied_forks[i].forknum); + hash_seq_init(&seq, applied_forks); + while ((fork = hash_seq_search(&seq)) != NULL) + smgrimmedsync(smgropen(fork->rlocator, INVALID_PROC_NUMBER), + fork->forknum); ereport(LOG, (errmsg("double write buffer recovery: %d of %d candidate pages restored, generation " UINT64_FORMAT, n_applied, n_candidates, control->generation))); hash_destroy(candidates); - pfree(applied_forks); + hash_destroy(applied_forks); pfree(meta_buf); pfree(image_buf); pfree(disk_buf); @@ -313,6 +350,13 @@ DWBApplyPass(const DWBControlFileData *control) * Durably remove the contents of pg_dwb/, keeping the directory (or the * symlink to it) in place. Used when a restored backup ships a foreign * ring and when the geometry GUCs changed. + * + * The control file goes first, durably: a crash in the middle of the batch + * sweep must not leave a readable control beside missing batch files, or a + * retried apply-pass would hard-fail on the ENOENT forever. With the + * control gone first, a retry takes the cold-create path (which itself + * wipes leftovers) — correct for both callers, since the apply-pass, if + * one was needed, ran to completion before any wipe starts. */ static void DWBWipeRing(void) @@ -328,6 +372,13 @@ DWBWipeRing(void) errmsg("could not stat directory \"%s\": %m", DWB_DIR))); } + if (unlink(DWB_CONTROL_FILE) < 0 && errno != ENOENT) + ereport(FATAL, + (errcode_for_file_access(), + errmsg("could not remove file \"%s\": %m", + DWB_CONTROL_FILE))); + fsync_fname(DWB_DIR, true); + if (!rmtree(DWB_DIR, false)) ereport(FATAL, (errcode_for_file_access(), @@ -344,29 +395,39 @@ DWBWipeRing(void) * metas. Returns the highest LSN the apply-pass wrote to a data file, or * InvalidXLogRecPtr. * - * unclean_start is the caller's pg_control verdict (neither DB_SHUTDOWNED - * state). The apply-pass additionally keys on the ring's own RING_CLEAN - * marker: a standby's shutdown restartpoint can be skipped entirely, - * leaving retirement fsyncs pending, and then only the marker knows the - * ring still covers data writes that may not have reached disk. + * Whether the apply-pass must run is decided by the ring's own RING_CLEAN + * marker alone, never by the pg_control state. The marker is the exact + * certificate: it is set only after full retirement (so while it is set, + * no slot covers a data write that has not reached disk) and cleared + * before the ring reopens (so slots of the clearing run are covered until + * the next clean shutdown re-sets it). pg_control can be both cleaner and + * dirtier than the ring: a standby's shutdown restartpoint can be skipped + * entirely, leaving retirement fsyncs pending behind a clean pg_control — + * and a crash under an interim full_pages/off run (which touches neither + * the marker nor the generation) leaves an unclean pg_control over a fully + * retired ring whose stale slots still match the current generation, where + * an apply would resurrect ancient pages over blocks torn long after the + * ring was closed. * * restoring_backup means the data directory is a restored base backup - * (backup_label present, or pg_control still carries backupStartPoint - * after a crash mid-backup-recovery). Any ring found in that case was - * shipped by a third-party backup tool and must not be applied: its slots - * carry the restored control's own generation, and the restored data files - * are legitimately older than the slot copies, so both staleness defences - * pass — an unguarded apply would push pages from the future of the backup - * into a PITR target. The WAL of the backup window carries forced full - * page images instead, so the ring is not needed for this recovery; it is + * (backup_label present, or pg_control still carrying backupStartPoint + * after a crash mid-backup-recovery). A ring found in that case — shipped + * by a third-party backup tool, or this server's own from a crashed + * backup-recovery run — must not be applied: its slots carry the restored + * control's own generation, and the restored data files are legitimately + * older than the slot copies, so both staleness defences pass — an + * unguarded apply would push pages from the future of the backup into a + * PITR target. The WAL of the backup window carries forced full page + * images instead, so the ring is not needed for this recovery; it is * wiped and recreated cold. */ XLogRecPtr -DWBStartup(bool unclean_start, bool restoring_backup) +DWBStartup(bool restoring_backup) { DWBControlFileData control; XLogRecPtr applied_upto = InvalidXLogRecPtr; bool created = false; + bool corrupt; bool need_apply; if (!DWBIsEnabled()) @@ -388,6 +449,8 @@ DWBStartup(bool unclean_start, bool restoring_backup) * lying dormant: a much later switch to double_writes would find * it with a plausible control file. Discard it now. */ + ereport(LOG, + (errmsg("discarding double write buffer ring contents restored from a base backup"))); DWBWipeRing(); return InvalidXLogRecPtr; } @@ -400,16 +463,35 @@ DWBStartup(bool unclean_start, bool restoring_backup) * failure under data_sync_retry) leaves RING_CLEAN unset, and the * pending data writes it covers are exactly as unprotected. */ - if (DWBReadControlFile(&control, true) && - (control.flags & DWB_CONTROL_RING_CLEAN) == 0) + if (DWBReadControlFile(&control, true, &corrupt)) + { + if ((control.flags & DWB_CONTROL_RING_CLEAN) == 0) + ereport(FATAL, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("the double write buffer ring was not cleanly shut down, cannot start with \"io_torn_pages_protection=%s\"", + DWBProtectionModeName(io_torn_pages_protection)), + errdetail("The ring in \"%s\" may hold repairs of torn data pages that have not been applied.", + DWB_DIR), + errhint("Start the server once with \"io_torn_pages_protection=double_writes\" and shut it down cleanly, or remove \"%s\" if you accept the risk of torn data pages.", + DWB_DIR))); + } + else if (corrupt) + { + /* + * An unreadable ring state must not block modes that never touch + * the ring with a bare low-level error: name the way out. (A + * double_writes start would refuse too, so the only cure is + * removal.) + */ ereport(FATAL, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("the double write buffer ring was not cleanly shut down, cannot start with \"io_torn_pages_protection=%s\"", + errmsg("the double write buffer ring state could not be validated, cannot start with \"io_torn_pages_protection=%s\"", DWBProtectionModeName(io_torn_pages_protection)), - errdetail("The ring in \"%s\" may hold repairs of torn data pages that have not been applied.", + errdetail("The control file in \"%s\" is unreadable or corrupt, and the ring may hold repairs of torn data pages that have not been applied.", DWB_DIR), - errhint("Start the server once with \"io_torn_pages_protection=double_writes\" and shut it down cleanly, or remove \"%s\" if you accept the risk of torn data pages.", + errhint("Remove \"%s\" if you accept the risk of torn data pages.", DWB_DIR))); + } return InvalidXLogRecPtr; } @@ -427,22 +509,27 @@ DWBStartup(bool unclean_start, bool restoring_backup) DWBWipeRing(); } - if (!DWBReadControlFile(&control, true)) + if (!DWBReadControlFile(&control, true, NULL)) { - /* cold start: no ring yet */ + /* + * Cold start: no ring yet. Sweep the directory first — an + * interrupted wipe can leave batch files behind after the control + * file is gone. + */ + DWBWipeRing(); DWBCreateRing(); created = true; - if (!DWBReadControlFile(&control, false)) + if (!DWBReadControlFile(&control, false, NULL)) pg_unreachable(); } /* - * The pg_control verdict alone is not enough: see the RING_CLEAN - * discussion in the header comment. A freshly created ring has nothing - * to apply even though its marker is unset. + * The RING_CLEAN marker alone decides (see the DWBStartup comment above + * for why pg_control must not weigh in). A freshly created ring has + * nothing to apply even though its marker is unset. */ need_apply = !created && - (unclean_start || (control.flags & DWB_CONTROL_RING_CLEAN) == 0); + (control.flags & DWB_CONTROL_RING_CLEAN) == 0; if (!created && (control.num_batches != (uint32) dwb_num_batches || @@ -463,7 +550,7 @@ DWBStartup(bool unclean_start, bool restoring_backup) dwb_num_batches, dwb_batch_pages))); DWBWipeRing(); DWBCreateRing(); - if (!DWBReadControlFile(&control, false)) + if (!DWBReadControlFile(&control, false, NULL)) pg_unreachable(); } else if (need_apply) @@ -532,7 +619,7 @@ DWBMarkCleanShutdown(void) } } - if (!DWBReadControlFile(&control, false)) + if (!DWBReadControlFile(&control, false, NULL)) pg_unreachable(); control.flags |= DWB_CONTROL_RING_CLEAN; control.crc = DWBControlCrc(&control); diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 60ae0c0a911d5..5a145be30df8f 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -356,6 +356,7 @@ static const struct config_enum_entry synchronous_commit_options[] = { {NULL, 0, false} }; +/* keep the spellings in sync with DWBProtectionModeName() in pg_control.h */ static const struct config_enum_entry io_torn_pages_protection_options[] = { {"off", DWB_PROTECT_OFF, false}, {"full_pages", DWB_PROTECT_FULL_PAGES, false}, diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c index 41b1a3137a71b..63d368d874ec8 100644 --- a/src/bin/pg_controldata/pg_controldata.c +++ b/src/bin/pg_controldata/pg_controldata.c @@ -296,6 +296,8 @@ main(int argc, char *argv[]) ControlFile->backupEndRequired ? _("yes") : _("no")); printf(_("wal_level setting: %s\n"), wal_level_str(ControlFile->wal_level)); + printf(_("io_torn_pages_protection setting: %s\n"), + DWBProtectionModeName(ControlFile->io_torn_pages_protection)); printf(_("wal_log_hints setting: %s\n"), ControlFile->wal_log_hints ? _("on") : _("off")); printf(_("max_connections setting: %d\n"), @@ -310,8 +312,6 @@ main(int argc, char *argv[]) ControlFile->max_locks_per_xact); printf(_("track_commit_timestamp setting: %s\n"), ControlFile->track_commit_timestamp ? _("on") : _("off")); - printf(_("io_torn_pages_protection setting: %s\n"), - DWBProtectionModeName(ControlFile->io_torn_pages_protection)); printf(_("Maximum data alignment: %u\n"), ControlFile->maxAlign); /* we don't print floatFormat since can't say much useful about it */ diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c index bc9795dc1efc6..6aed326a9a5ef 100644 --- a/src/bin/pg_rewind/pg_rewind.c +++ b/src/bin/pg_rewind/pg_rewind.c @@ -330,7 +330,9 @@ main(int argc, char **argv) * any way. A stopped source has no such requirement. The mode is read * from the source's pg_control — the authoritative record, unlike the * legacy full_page_writes GUC, which only matters under "full_pages" and - * is checked in init_libpq_source. + * is checked by init_libpq_conn when the connection is made (so a + * double_writes source with full_page_writes=off draws that message, not + * this one). */ if (connstr_source) { diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h index 50d627f2d520e..62d52a53e4464 100644 --- a/src/include/access/xlog_internal.h +++ b/src/include/access/xlog_internal.h @@ -278,9 +278,9 @@ typedef struct xl_parameter_change int max_prepared_xacts; int max_locks_per_xact; int wal_level; + int io_torn_pages_protection; /* DWBTornPageProtection */ bool wal_log_hints; bool track_commit_timestamp; - int io_torn_pages_protection; } xl_parameter_change; /* logs restore point */ diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h index d98d6fb8081ef..076d5c568794c 100644 --- a/src/include/catalog/pg_control.h +++ b/src/include/catalog/pg_control.h @@ -30,23 +30,29 @@ /* * The torn-page protection mechanism (GUC io_torn_pages_protection). Like * wal_level, the value in force on the WAL-generating server is a protocol - * fact: it decides whether the WAL carries full page images, so it is - * recorded in pg_control and in XLOG_PARAMETER_CHANGE records for replay to - * track. Defined here rather than in storage/dwb.h so that frontend code - * reading pg_control can use it. + * fact: it decides whether the WAL can carry full page images at all (under + * "full_pages" the legacy full_page_writes GUC still chooses whether it + * actually does), so it is recorded in pg_control and in + * XLOG_PARAMETER_CHANGE records for replay to track. Defined here rather + * than in storage/dwb.h so that frontend code reading pg_control can use it. + * + * The numeric values are stored on disk and in WAL; never renumber the + * members. Keep the names in sync with the GUC option list in + * guc_tables.c. */ typedef enum { - DWB_PROTECT_OFF, - DWB_PROTECT_FULL_PAGES, - DWB_PROTECT_DOUBLE_WRITES, + DWB_PROTECT_OFF = 0, + DWB_PROTECT_FULL_PAGES = 1, + DWB_PROTECT_DOUBLE_WRITES = 2, } DWBTornPageProtection; /* GUC-spelling name of a DWBTornPageProtection value, for messages */ static inline const char * DWBProtectionModeName(int mode) { - switch (mode) + /* the cast keeps -Wswitch honest about newly added members */ + switch ((DWBTornPageProtection) mode) { case DWB_PROTECT_OFF: return "off"; @@ -55,6 +61,7 @@ DWBProtectionModeName(int mode) case DWB_PROTECT_DOUBLE_WRITES: return "double_writes"; } + /* garbage read from disk or WAL must not turn into UB */ return "unrecognized"; } @@ -207,6 +214,7 @@ typedef struct ControlFileData * or hot standby. */ int wal_level; + int io_torn_pages_protection; /* DWBTornPageProtection */ bool wal_log_hints; int MaxConnections; int max_worker_processes; @@ -214,7 +222,6 @@ typedef struct ControlFileData int max_prepared_xacts; int max_locks_per_xact; bool track_commit_timestamp; - int io_torn_pages_protection; /* DWBTornPageProtection */ /* * This data is used to check for hardware-architecture compatibility of diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 77cfc0227b7ac..614002760479f 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -108,7 +108,11 @@ typedef struct DWBControlFileData uint32 batch_pages; uint32 flags; /* DWB_CONTROL_* */ uint64 generation; /* apply-pass horizon: bumped durably on every - * start before the ring opens */ + * double_writes start before the ring opens. + * Monotonic within one ring incarnation; a + * geometry-change recreate restarts it at + * zero, which is safe because the wipe leaves + * no CRC-valid slot behind */ pg_crc32c crc; /* CRC of all preceding fields */ } DWBControlFileData; @@ -116,9 +120,16 @@ typedef struct DWBControlFileData * DWBControlFileData.flags. RING_CLEAN certifies that every data-file write * covered by an on-disk slot had been fsynced when the server shut down: it * is written at the end of a clean shutdown after the ring is fully retired, - * and cleared by the next startup before the ring reopens. While it is set, - * the ring holds no unapplied repairs, so the apply-pass can be skipped and - * a start under a different io_torn_pages_protection mode is legal. + * and cleared by the next double_writes startup before the ring reopens. + * While it is set, the ring holds no unapplied repairs, so the apply-pass + * must be skipped (non-double_writes runs in between leave the generation + * untouched, so old slots would otherwise still match it) and a start under + * a different io_torn_pages_protection mode is legal. + * + * The flags field occupies what was interior alignment padding in version-1 + * control files; those read back with flags == 0 (the padding was always + * memset and CRC-covered), which is the safe "not clean" state, so filling + * the hole needed no DWB_VERSION bump. */ #define DWB_CONTROL_RING_CLEAN 0x0001 @@ -126,7 +137,11 @@ typedef struct DWBBatchHeader { uint32 magic; uint32 version; - uint64 batch_id; + uint64 batch_id; /* incarnation id, monotonic in publication + * order within one server run (next_batch_id + * restarts at 1 with each start); the + * apply-pass relies on this to break LSN ties + * between slots of one generation */ uint32 n_slots; /* capped_slots at seal time */ pg_crc32c crc; /* CRC of all preceding fields */ } DWBBatchHeader; @@ -374,7 +389,9 @@ pg_noreturn extern void DWBRetireWorkerMain(Datum main_arg); /* dwb_file.c */ extern void DWBCreateRing(void); -extern bool DWBReadControlFile(DWBControlFileData *control, bool missing_ok); +extern void DWBBatchFilePath(char *path, int batch_idx); +extern bool DWBReadControlFile(DWBControlFileData *control, bool missing_ok, + bool *corruptp); extern void DWBWriteControlFile(const DWBControlFileData *control); extern int DWBOpenBatchFile(int batch_idx); extern void DWBPrepareBatchWrite(int batch_idx); @@ -387,7 +404,7 @@ extern pg_crc32c DWBControlCrc(const DWBControlFileData *control); extern pg_crc32c DWBBatchHeaderCrc(const DWBBatchHeader *hdr); /* dwb_recovery.c */ -extern XLogRecPtr DWBStartup(bool unclean_start, bool restoring_backup); +extern XLogRecPtr DWBStartup(bool restoring_backup); extern void DWBMarkCleanShutdown(void); #endif /* DWB_H */ diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index 76a539e4ac494..8aeec8c37c2ac 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -47,6 +47,7 @@ tests += { 't/008_modes.pl', 't/009_fpw_transition.pl', 't/010_recovery.pl', + 't/011_geometry_recovery.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/001_dwb.pl b/src/test/modules/test_dwb/t/001_dwb.pl index 17f2346e54924..a43b5e51f101c 100644 --- a/src/test/modules/test_dwb/t/001_dwb.pl +++ b/src/test/modules/test_dwb/t/001_dwb.pl @@ -246,7 +246,8 @@ sub flip_byte # The on-disk layout follows the geometry GUCs, so a change rebuilds the # ring from scratch (after applying the old one if needed — exercised in -# t/010_recovery.pl); the generation restarts with the fresh control file. +# t/011_geometry_recovery.pl); the generation restarts with the fresh +# control file. $node->stop; my $log_offset = -s $node->logfile; $node->append_conf('postgresql.conf', 'dwb_num_batches = 32'); diff --git a/src/test/modules/test_dwb/t/005_standby.pl b/src/test/modules/test_dwb/t/005_standby.pl index b522905f68837..7d274f2fd6dca 100644 --- a/src/test/modules/test_dwb/t/005_standby.pl +++ b/src/test/modules/test_dwb/t/005_standby.pl @@ -155,6 +155,65 @@ is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), '10002', 'replication resumed after the primary crash'); +# --- a torn page on the standby is repaired by its own apply-pass --------- + +# The apply-pass on a crashed standby runs against pg_control state +# DB_IN_ARCHIVE_RECOVERY — the branch that may raise minRecoveryPoint — +# and must repair from the standby's OWN ring: the replayed WAL carries no +# page images that could do it instead. +sub read_block +{ + my ($file, $blkno) = @_; + my $buf; + + open my $fh, '<:raw', $file or die "could not open $file: $!"; + sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; + sysread($fh, $buf, 8192) == 8192 or die "short read from $file: $!"; + close $fh; + return $buf; +} + +sub write_block +{ + my ($file, $blkno, $buf) = @_; + + open my $fh, '+<:raw', $file or die "could not open $file: $!"; + sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; + syswrite($fh, $buf) == length($buf) or die "short write to $file: $!"; + close $fh; + return; +} + +$primary->safe_psql('postgres', q( + CREATE TABLE ts_repair AS SELECT g AS id FROM generate_series(1, 100) g; +)); +$primary->safe_psql('postgres', 'CHECKPOINT'); +$primary->wait_for_catchup($standby); +my $ts_path = $primary->safe_psql('postgres', + "SELECT pg_relation_filepath('ts_repair')"); +my $ts_relnum = $primary->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE relname = 'ts_repair'"); + +# a restartpoint flushes the replayed pages through the standby's ring; +# crash right after, while the table's slot is still on disk +$standby->append_conf('postgresql.conf', 'log_min_messages = debug1'); +$standby->safe_psql('postgres', 'CHECKPOINT'); +$standby->stop('immediate'); + +my $ts_file = $standby->data_dir . '/' . $ts_path; +write_block($ts_file, 0, + substr(read_block($ts_file, 0), 0, 4096) . ("\0" x 4096)); + +$standby_log_offset = -s $standby->logfile; +$standby->start; +ok( $standby->log_contains( + qr!restoring page 0 of relation \d+/\d+/$ts_relnum fork 0!, + $standby_log_offset), + 'the crashed standby repaired its torn page from its own ring'); +$primary->wait_for_catchup($standby); +is( $standby->safe_psql('postgres', 'SELECT count(*) FROM ts_repair'), + '100', 'the repaired standby page reads whole'); + # --- promotion with a replay backlog ------------------------------------- # Pause replay, pile up a burst, make sure it is flushed to the standby's diff --git a/src/test/modules/test_dwb/t/006_backup.pl b/src/test/modules/test_dwb/t/006_backup.pl index cf0b018a5bcab..65940677fee61 100644 --- a/src/test/modules/test_dwb/t/006_backup.pl +++ b/src/test/modules/test_dwb/t/006_backup.pl @@ -129,8 +129,9 @@ # there — the slots carry the restored control's own generation, and the # restored data files are legitimately older than the slot copies — so a # start from a base backup (backup_label present) must not apply the ring: -# it is wiped and recreated cold. The planted control file is garbage, -# which without the guard would be a fatal checksum error. +# it is wiped and recreated cold. The planted control file is short +# garbage; reading it would be a fatal "read 16 of 40" error, so this also +# pins that the wipe comes before any ring-state read. my $planted = PostgreSQL::Test::Cluster->new('dwb_planted'); $planted->init_from_backup($node, 'content_check'); ok(-f $planted->data_dir . '/backup_label', @@ -167,6 +168,34 @@ 'a later crash of the restored cluster applies the ring normally'); $planted->stop; +# --- the wipe also covers restores in the non-ring modes ------------------ + +# A planted ring is dangerous even to a cluster restored under +# "full_pages": left dormant, it would greet a much later switch to +# double_writes with a plausible control file. The restore start must +# discard it — before any ring-state read, and without tripping the +# downgrade guard on it. +my $planted_fp = PostgreSQL::Test::Cluster->new('dwb_planted_fp'); +$planted_fp->init_from_backup($node, 'content_check'); +$planted_fp->append_conf('postgresql.conf', + 'io_torn_pages_protection = full_pages'); +append_to_file($planted_fp->data_dir . '/pg_dwb/control', 'torn by the tool'); +append_to_file($planted_fp->data_dir . '/pg_dwb/batch_9999', 'foreign slots'); + +my $planted_fp_log_offset = -s $planted_fp->logfile; +$planted_fp->start; +ok( $planted_fp->log_contains( + qr/discarding double write buffer ring contents restored from a base backup/, + $planted_fp_log_offset), + 'a full_pages restore discards the planted ring too'); +ok(!-f $planted_fp->data_dir . '/pg_dwb/batch_9999', + '... removing the foreign files'); +ok( !$planted_fp->log_contains(qr/ring opened/, $planted_fp_log_offset), + '... without creating a ring it will not use'); +is( $planted_fp->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpi'), + '100', 'restored data is intact under full_pages'); +$planted_fp->stop; + # --- pg_dwb as a symlink backs up as an empty real directory -------------- SKIP: diff --git a/src/test/modules/test_dwb/t/008_modes.pl b/src/test/modules/test_dwb/t/008_modes.pl index 868153190afa8..4a313d965b41a 100644 --- a/src/test/modules/test_dwb/t/008_modes.pl +++ b/src/test/modules/test_dwb/t/008_modes.pl @@ -8,6 +8,7 @@ use strict; use warnings FATAL => 'all'; +use File::Path qw(rmtree); use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; @@ -134,4 +135,51 @@ sub wal_window ok( !$node->log_contains(qr/ring opened/, $log_offset), '... and the leftover ring stays closed'); +# --- reopening the ring re-arms the guard -------------------------------- + +# A double_writes start clears RING_CLEAN before the ring reopens, so a +# crash of that run leaves the marker unset and the guard must fire again: +# the marker certifies one clean closure, not a permanent state. +$node->stop; +$node->append_conf('postgresql.conf', + 'io_torn_pages_protection = double_writes'); +$node->start; +$node->stop('immediate'); + +$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +$ret = $node->start(fail_ok => 1); +is($ret, 0, 'a crash after reopening the ring re-arms the guard'); + +# close it cleanly once more +$node->append_conf('postgresql.conf', + 'io_torn_pages_protection = double_writes'); +$node->start; +$node->stop; + +# --- a corrupt ring control is refused, with a way out ------------------- + +# Modes that never touch the ring must still refuse an unreadable control +# (the ring may hold unapplied repairs), but with a message naming the +# removal recipe instead of a bare low-level read error. +my $control = $node->data_dir . '/pg_dwb/control'; +open my $fh, '>', $control or die "open $control: $!"; +binmode $fh; +print $fh "\x00" x 16; +close $fh; + +$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +$log_offset = -s $node->logfile; +$ret = $node->start(fail_ok => 1); +is($ret, 0, 'a corrupt ring control refuses a full_pages start'); +ok( $node->log_contains( + qr/FATAL: .* the double write buffer ring state could not be validated, cannot start with "io_torn_pages_protection=full_pages"/, + $log_offset), + '... naming the ring state as the problem'); + +# the hint's recipe: removing pg_dwb unblocks the start +rmtree($node->data_dir . '/pg_dwb'); +$node->start; +is( $node->safe_psql('postgres', 'SHOW io_torn_pages_protection'), + 'full_pages', 'removing pg_dwb unblocks the non-ring mode'); + done_testing(); diff --git a/src/test/modules/test_dwb/t/010_recovery.pl b/src/test/modules/test_dwb/t/010_recovery.pl index 52ab1f7a69d1a..d72eb37dbf342 100644 --- a/src/test/modules/test_dwb/t/010_recovery.pl +++ b/src/test/modules/test_dwb/t/010_recovery.pl @@ -203,4 +203,61 @@ sub write_block is( $node->safe_psql('postgres', 'SELECT count(*) FROM tstale'), '100', 'page manually restored, cluster consistent'); +# --- the marker alone triggers the pass, not the pg_control state -------- + +# Leave a crashed ring behind a CLEAN pg_control: stash the ring right +# after a crash, run a clean stop cycle, then put the crashed ring back. +# Only the unset RING_CLEAN marker knows this ring was never retired — a +# standby whose shutdown restartpoint was skipped leaves exactly this +# combination, and the pass must key on the marker, not on pg_control. +$node->safe_psql('postgres', q( + CREATE TABLE tmark AS SELECT g AS id FROM generate_series(1, 100) g; +)); +$node->safe_psql('postgres', 'CHECKPOINT'); +my $tmark_file = + $node->data_dir . '/' + . $node->safe_psql('postgres', "SELECT pg_relation_filepath('tmark')"); +$node->stop('immediate'); + +my $mark_stash = $node->basedir . '/mark_stash'; +PostgreSQL::Test::RecursiveCopy::copypath($node->data_dir . '/pg_dwb', + $mark_stash); + +$node->start; +$node->stop; + +rmtree($node->data_dir . '/pg_dwb'); +PostgreSQL::Test::RecursiveCopy::copypath($mark_stash, + $node->data_dir . '/pg_dwb'); +write_block($tmark_file, 0, + substr(read_block($tmark_file, 0), 0, 4096) . ("\0" x 4096)); + +$log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains( + qr/double write buffer recovery: 1 of 1 candidate pages restored/, + $log_offset), + 'unretired ring is applied despite a clean pg_control'); +is( $node->safe_psql('postgres', 'SELECT count(*) FROM tmark'), + '100', 'torn page behind a clean shutdown is whole again'); + +# --- a slot for a dropped relation is skipped ---------------------------- + +# The relation's file may survive as an empty tombstone until the next +# checkpoint, or be gone entirely; either way there is nothing to repair +# and the pass must not trip over it. +$node->safe_psql('postgres', q( + CREATE TABLE tdrop AS SELECT g AS id FROM generate_series(1, 100) g; +)); +$node->safe_psql('postgres', 'CHECKPOINT'); +$node->safe_psql('postgres', 'DROP TABLE tdrop'); +$node->stop('immediate'); + +$log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains( + qr/double write buffer recovery: 0 of 1 candidate pages restored/, + $log_offset), + 'a candidate for a dropped relation is counted but skipped'); + done_testing(); diff --git a/src/test/modules/test_dwb/t/011_geometry_recovery.pl b/src/test/modules/test_dwb/t/011_geometry_recovery.pl new file mode 100644 index 0000000000000..1e7693c71601b --- /dev/null +++ b/src/test/modules/test_dwb/t/011_geometry_recovery.pl @@ -0,0 +1,94 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# A geometry change over a ring that was not cleanly closed: the apply-pass +# must run with the OLD geometry recorded in pg_dwb/control — not the new +# GUCs — before the ring is recreated. A regression that read the GUCs +# instead would compute a wrong meta-region size, fail every slot CRC, +# report nothing to restore and leave the damage in place, silently: only +# the combination "page repaired AND ring recreated in one start" pins the +# ordering. +# +# The damage technique follows t/010_recovery.pl: dwb_retire_workers = 0 +# retires every batch on the spot, so the last page flushed before the +# crash — the user table's, sorted last in BufferSync by relfilenode — is +# the one slot still on disk. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_geometry_recovery'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 0 +dwb_batch_timeout_ms = 20 +autovacuum = off +bgwriter_lru_maxpages = 0 +)); +$node->start; + +sub read_block +{ + my ($file, $blkno) = @_; + my $buf; + + open my $fh, '<:raw', $file or die "could not open $file: $!"; + sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; + sysread($fh, $buf, 8192) == 8192 or die "short read from $file: $!"; + close $fh; + return $buf; +} + +sub write_block +{ + my ($file, $blkno, $buf) = @_; + + open my $fh, '+<:raw', $file or die "could not open $file: $!"; + sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; + syswrite($fh, $buf) == length($buf) or die "short write to $file: $!"; + close $fh; + return; +} + +$node->safe_psql('postgres', q( + CREATE TABLE tgeo AS SELECT g AS id FROM generate_series(1, 100) g; +)); +$node->safe_psql('postgres', 'CHECKPOINT'); +my $tgeo_file = + $node->data_dir . '/' + . $node->safe_psql('postgres', "SELECT pg_relation_filepath('tgeo')"); + +# stale-page damage: put the pre-update image back after the crash, so the +# repair can only come from the ring copy's higher LSN +my $tgeo_v1 = read_block($tgeo_file, 0); +$node->safe_psql('postgres', 'UPDATE tgeo SET id = id + 1000 WHERE id <= 50'); +$node->safe_psql('postgres', 'CHECKPOINT'); +$node->stop('immediate'); + +write_block($tgeo_file, 0, $tgeo_v1); + +$node->append_conf('postgresql.conf', 'dwb_batch_pages = 32'); +my $log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains( + qr/double write buffer recovery: 1 of 1 candidate pages restored/, + $log_offset), + 'the crashed ring is applied with its recorded geometry'); +ok( $node->log_contains( + qr/recreating double write buffer ring: geometry changed from 16 batches of 16 pages to 16 batches of 32 pages/, + $log_offset), + '... and only then recreated under the new GUCs'); +ok( $node->log_contains( + qr/ring opened: 16 batches of 32 pages, generation 1\b/, $log_offset), + '... with a fresh generation'); +is( $node->safe_psql('postgres', 'SELECT count(*) FROM tgeo WHERE id > 1000'), + '50', 'the stale page carries the update again'); + +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index ff62a855bdc9f..8ea81b413495b 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -199,7 +199,7 @@ count_ring_slots(bool current_only, bool have_filter, Oid relnumber) char *image; int valid = 0; - if (!DWBReadControlFile(&control, false)) + if (!DWBReadControlFile(&control, false, NULL)) pg_unreachable(); meta_region = DWBMetaRegionSize(control.batch_pages); From 5f575d20d94103ba56898c3f11a088acb1f62250 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sun, 26 Jul 2026 00:14:44 +0300 Subject: [PATCH 15/52] Close the review-verification findings on the Stage 4 follow-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The verification round over the previous commit confirmed every original finding fixed and surfaced a handful of new ones, all closed here. Re-bump PG_CONTROL_VERSION (1802) and XLOG_PAGE_MAGIC (0xD11A): the previous commit moved io_torn_pages_protection next to wal_level in ControlFileData and xl_parameter_change without changing the version constants it had already claimed, so a data directory initialized on the previous commit would be reinterpreted field-by-field with a matching CRC and no complaint. Limit the crash-under-"off" warning to servers whose crashed run owned the pg_control mode field: on a standby the field describes the primary, and a local double_writes standby of an "off" primary would have drawn the warning on every crash while its own ring repaired the pages the message declared unrepairable. Make DWBWipeRing report whether it removed anything and log the "discarding ring contents" message only then — a restored base backup normally ships pg_dwb/ empty, and every ordinary restore start claimed to have discarded contents that never existed. The empty-directory early return also makes the second wipe of a double_writes restore a no-op. Keep a too-new ring format version on its own FATAL instead of folding it into the corrupt-control refusal: that ring is intact and the "remove pg_dwb" hint would invite discarding it. The remaining corrupt paths now log the specific low-level cause (errno, short read, bad checksum) before the caller's summary FATAL, and the previously unchecked close in the short-read branch is reported too. Comment accuracy, per the re-review: the batch_id tie-break is a deterministic pick among hint-bit-equivalent copies, not a strict later-copy guarantee (ids are assigned at batch open and two writer classes can invert the order); the PageIsNew-skip rationale now covers the all-zero staged image FlushBuffer can produce; the RING_CLEAN contract states that any future ring-opening path must clear the marker in the same control write; DWBWipeRing's header names all its callers; the file header no longer claims non-ring modes never touch the ring. New tests: t/008 pins the suppressing direction of the marker keying — a crash under an interim full_pages run must not re-arm the apply-pass — and the warning after a crash under "off"; t/010 pins that an all-zero on-disk page is never repaired from a slot, bytewise. --- src/backend/access/transam/xlog.c | 11 ++- src/backend/storage/dwb/dwb_file.c | 49 +++++++--- src/backend/storage/dwb/dwb_recovery.c | 102 +++++++++++++------- src/include/access/xlog_internal.h | 2 +- src/include/catalog/pg_control.h | 2 +- src/include/storage/dwb.h | 20 ++-- src/test/modules/test_dwb/t/008_modes.pl | 35 +++++++ src/test/modules/test_dwb/t/010_recovery.pl | 32 ++++++ 8 files changed, 195 insertions(+), 58 deletions(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index b03ebe6f5b0bb..c1f467c4df845 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -5651,11 +5651,16 @@ StartupXLOG(void) /* * Crash recovery over WAL generated without any torn page protection * cannot repair pages the crash tore, whatever the local mode says - * now. The mode-based FATAL in CheckRequiredParameterValues covers - * archive recovery only, so this is the one transition that would - * otherwise be silent. + * now. The mode-based FATAL in CheckRequiredParameterValues fires + * for archive recovery only, so this is the one transition that would + * otherwise be silent. On a standby the pg_control field describes + * the primary, not the run that crashed here — and a local + * double_writes standby of an "off" primary repairs its own torn + * pages from its ring — so the warning is limited to servers whose + * crashed run owned the field. */ if (didCrash && + ControlFile->state != DB_IN_ARCHIVE_RECOVERY && ControlFile->io_torn_pages_protection == DWB_PROTECT_OFF && io_torn_pages_protection != DWB_PROTECT_OFF) ereport(WARNING, diff --git a/src/backend/storage/dwb/dwb_file.c b/src/backend/storage/dwb/dwb_file.c index a08ba911e2f08..663c161e33505 100644 --- a/src/backend/storage/dwb/dwb_file.c +++ b/src/backend/storage/dwb/dwb_file.c @@ -93,8 +93,11 @@ DWBBatchFilePath(char *path, int batch_idx) * a damaged control file must not silently degrade the apply-pass. * * A caller that can refuse startup with a more helpful message than the - * low-level FATALs may pass corruptp: any failure other than a tolerated - * ENOENT then sets *corruptp and returns false instead. + * low-level FATALs may pass corruptp: an unreadable or corrupt file then + * sets *corruptp and returns false instead, with the specific cause + * reported at LOG so it is not lost behind the caller's summary. A + * too-new format version is FATAL either way: that ring is intact, and + * "corrupt" advice would invite discarding it. */ bool DWBReadControlFile(DWBControlFileData *control, bool missing_ok, @@ -113,6 +116,10 @@ DWBReadControlFile(DWBControlFileData *control, bool missing_ok, return false; if (corruptp) { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", + DWB_CONTROL_FILE))); *corruptp = true; return false; } @@ -129,8 +136,24 @@ DWBReadControlFile(DWBControlFileData *control, bool missing_ok, { if (corruptp) { + /* distinguish a real read error from a truncated file */ + if (r < 0) + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", + DWB_CONTROL_FILE))); + else + ereport(LOG, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("could not read file \"%s\": read %d of %zu", + DWB_CONTROL_FILE, r, + sizeof(DWBControlFileData)))); + if (CloseTransientFile(fd) != 0) + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", + DWB_CONTROL_FILE))); *corruptp = true; - CloseTransientFile(fd); return false; } /* distinguish a real read error from a truncated file */ @@ -150,25 +173,27 @@ DWBReadControlFile(DWBControlFileData *control, bool missing_ok, errmsg("could not close file \"%s\": %m", DWB_CONTROL_FILE))); if (control->magic != DWB_CONTROL_MAGIC || - !EQ_CRC32C(control->crc, DWBControlCrc(control)) || - control->min_version > DWB_VERSION) + !EQ_CRC32C(control->crc, DWBControlCrc(control))) { if (corruptp) { - *corruptp = true; - return false; - } - if (control->magic != DWB_CONTROL_MAGIC || - !EQ_CRC32C(control->crc, DWBControlCrc(control))) - ereport(FATAL, + ereport(LOG, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("invalid checksum or magic number in file \"%s\"", DWB_CONTROL_FILE))); + *corruptp = true; + return false; + } + ereport(FATAL, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("invalid checksum or magic number in file \"%s\"", + DWB_CONTROL_FILE))); + } + if (control->min_version > DWB_VERSION) ereport(FATAL, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("file \"%s\" requires format version at least %u, but this server supports %u", DWB_CONTROL_FILE, control->min_version, DWB_VERSION))); - } return true; } diff --git a/src/backend/storage/dwb/dwb_recovery.c b/src/backend/storage/dwb/dwb_recovery.c index 1a597a04c9298..b4e88577e4394 100644 --- a/src/backend/storage/dwb/dwb_recovery.c +++ b/src/backend/storage/dwb/dwb_recovery.c @@ -10,7 +10,8 @@ * writes, so slots left behind by the previous run can never masquerade as * current after a future crash. Order: read G -> (RING_CLEAN not set) * apply-pass over generation G + fsync -> durable control.generation := G+1 - * -> open ring. Starts in the other modes leave the ring untouched. + * -> open ring. Starts in the other modes leave the ring untouched, + * except that a restored base backup's ring contents are discarded. * * The apply-pass runs before WAL replay and repairs the data files * directly: a candidate slot must carry a valid meta_crc, the current @@ -31,6 +32,7 @@ */ #include "postgres.h" +#include #include #include #include @@ -49,11 +51,12 @@ typedef struct DWBApplyCandidate { BufferTag tag; /* hash key */ XLogRecPtr lsn; - uint64 batch_id; /* tie-breaker for equal LSNs: equal - * generation means one server run, where - * batch_id is monotonic in publication order - * (see DWBBatchHeader.batch_id), so the - * higher id holds the later copy */ + uint64 batch_id; /* tie-breaker for equal LSNs: equal-LSN + * copies of one generation can differ only in + * hint bits, so either is a valid redo base + * and the id (monotonic in batch-open order, + * see DWBBatchHeader.batch_id) just makes the + * pick deterministic */ uint32 batch_idx; uint32 slot_idx; pg_crc32c image_crc; /* revalidates the image on re-read */ @@ -71,7 +74,7 @@ StaticAssertDecl(sizeof(DWBAppliedFork) == "DWBAppliedFork has padding; unsafe as a HASH_BLOBS key"); static XLogRecPtr DWBApplyPass(const DWBControlFileData *control); -static void DWBWipeRing(void); +static bool DWBWipeRing(void); static bool DWBRingIsQuiescent(void); /* @@ -213,11 +216,14 @@ DWBApplyPass(const DWBControlFileData *control) continue; /* - * Keep the highest LSN; on equal LSNs the later batch wins (equal - * LSNs with different contents are real: a re-flush after - * hint-bit-only changes does not move the LSN). Within one batch - * the later slot wins by plain overwrite, matching the order the - * slots were filled in. + * Keep the highest LSN; on equal LSNs the higher batch_id wins + * (equal LSNs with different contents are real: a re-flush after + * hint-bit-only changes does not move the LSN — but such copies + * differ only in hint bits, so any of them is a valid redo base + * and the id merely makes the pick deterministic; with two writer + * classes a later flush can even land in an earlier- opened + * batch). Within one batch the later slot wins by plain + * overwrite, matching the order the slots were filled in. */ n_candidates++; entry = hash_search(candidates, &meta->tag, HASH_ENTER, &found); @@ -266,17 +272,20 @@ DWBApplyPass(const DWBControlFileData *control) smgrread(reln, forknum, blkno, disk_buf); /* - * A "new" page (empty header) is never repaired. Every staged image - * is an initialized page, so an empty on-disk header means the + * A "new" page (empty header) is never repaired. For a candidate + * holding an initialized image, an empty on-disk header means the * covered write's first sector never reached disk and the block had * never held an initialized page before — its init record therefore * lies after the last checkpoint, and replay recreates the page - * without reading the current contents. The skip is also required - * for correctness in the other direction: after a truncate + - * re-extend within one generation the ring can hold a pre-truncate - * copy of this block, and the re-extended zeroed page (LSN 0) would - * lose the LSN comparison below to that stale image, which nothing - * would then replay over. + * without reading the current contents. (A staged image can itself + * be all-zero — FlushBuffer may flush a still-new page — but such + * a copy carries LSN 0: skipping it here changes nothing, and on the + * repair branch below it would merely complete an intended zeroing.) + * The skip is also required for correctness in the other direction: + * after a truncate + re-extend within one generation the ring can + * hold a pre-truncate copy of this block, and the re-extended zeroed + * page (LSN 0) would lose the LSN comparison below to that stale + * image, which nothing would then replay over. */ if (PageIsNew((Page) disk_buf)) continue; @@ -348,30 +357,51 @@ DWBApplyPass(const DWBControlFileData *control) /* * Durably remove the contents of pg_dwb/, keeping the directory (or the - * symlink to it) in place. Used when a restored backup ships a foreign - * ring and when the geometry GUCs changed. + * symlink to it) in place, and report whether there was anything to + * remove — a restored backup normally ships pg_dwb/ empty, and the + * callers must not claim to have discarded ring contents that never + * existed. Callers: the restored-backup branches (a shipped ring must + * not survive), the geometry change, and the cold-create sweep that + * clears leftovers of an interrupted wipe. * * The control file goes first, durably: a crash in the middle of the batch * sweep must not leave a readable control beside missing batch files, or a * retried apply-pass would hard-fail on the ENOENT forever. With the - * control gone first, a retry takes the cold-create path (which itself - * wipes leftovers) — correct for both callers, since the apply-pass, if - * one was needed, ran to completion before any wipe starts. + * control gone first, a retry takes the cold-create path instead, which is + * correct for every caller, since the apply-pass, if one was needed, ran + * to completion before any wipe starts. */ -static void +static bool DWBWipeRing(void) { struct stat st; + DIR *dir; + struct dirent *de; + bool had_contents = false; if (lstat(DWB_DIR, &st) < 0) { if (errno == ENOENT) - return; + return false; ereport(FATAL, (errcode_for_file_access(), errmsg("could not stat directory \"%s\": %m", DWB_DIR))); } + dir = AllocateDir(DWB_DIR); + while ((de = ReadDir(dir, DWB_DIR)) != NULL) + { + if (strcmp(de->d_name, ".") != 0 && strcmp(de->d_name, "..") != 0) + { + had_contents = true; + break; + } + } + FreeDir(dir); + + if (!had_contents) + return false; + if (unlink(DWB_CONTROL_FILE) < 0 && errno != ENOENT) ereport(FATAL, (errcode_for_file_access(), @@ -385,6 +415,8 @@ DWBWipeRing(void) errmsg("could not remove contents of directory \"%s\"", DWB_DIR))); fsync_fname(DWB_DIR, true); + + return true; } /* @@ -449,9 +481,9 @@ DWBStartup(bool restoring_backup) * lying dormant: a much later switch to double_writes would find * it with a plausible control file. Discard it now. */ - ereport(LOG, - (errmsg("discarding double write buffer ring contents restored from a base backup"))); - DWBWipeRing(); + if (DWBWipeRing()) + ereport(LOG, + (errmsg("discarding double write buffer ring contents restored from a base backup"))); return InvalidXLogRecPtr; } @@ -504,9 +536,9 @@ DWBStartup(bool restoring_backup) if (restoring_backup) { - ereport(LOG, - (errmsg("discarding double write buffer ring contents restored from a base backup"))); - DWBWipeRing(); + if (DWBWipeRing()) + ereport(LOG, + (errmsg("discarding double write buffer ring contents restored from a base backup"))); } if (!DWBReadControlFile(&control, true, NULL)) @@ -516,7 +548,7 @@ DWBStartup(bool restoring_backup) * interrupted wipe can leave batch files behind after the control * file is gone. */ - DWBWipeRing(); + (void) DWBWipeRing(); DWBCreateRing(); created = true; if (!DWBReadControlFile(&control, false, NULL)) @@ -548,7 +580,7 @@ DWBStartup(bool restoring_backup) (errmsg("recreating double write buffer ring: geometry changed from %u batches of %u pages to %d batches of %d pages", control.num_batches, control.batch_pages, dwb_num_batches, dwb_batch_pages))); - DWBWipeRing(); + (void) DWBWipeRing(); DWBCreateRing(); if (!DWBReadControlFile(&control, false, NULL)) pg_unreachable(); diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h index 62d52a53e4464..f5fc6adf405dd 100644 --- a/src/include/access/xlog_internal.h +++ b/src/include/access/xlog_internal.h @@ -31,7 +31,7 @@ /* * Each page of XLOG file has a header like this: */ -#define XLOG_PAGE_MAGIC 0xD119 /* can be used as WAL version indicator */ +#define XLOG_PAGE_MAGIC 0xD11A /* can be used as WAL version indicator */ typedef struct XLogPageHeaderData { diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h index 076d5c568794c..041266a7402f4 100644 --- a/src/include/catalog/pg_control.h +++ b/src/include/catalog/pg_control.h @@ -22,7 +22,7 @@ /* Version identifier for this pg_control format */ -#define PG_CONTROL_VERSION 1801 +#define PG_CONTROL_VERSION 1802 /* Nonce key length, see below */ #define MOCK_AUTH_NONCE_LEN 32 diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 614002760479f..d14f4b9344972 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -124,7 +124,10 @@ typedef struct DWBControlFileData * While it is set, the ring holds no unapplied repairs, so the apply-pass * must be skipped (non-double_writes runs in between leave the generation * untouched, so old slots would otherwise still match it) and a start under - * a different io_torn_pages_protection mode is legal. + * a different io_torn_pages_protection mode is legal. Any new code path + * that opens the ring for writes must clear the marker in the same control + * write that bumps the generation — the marker-only apply decision is + * sound only while set-marker implies untouched-since-retirement. * * The flags field occupies what was interior alignment padding in version-1 * control files; those read back with flags == 0 (the padding was always @@ -137,11 +140,16 @@ typedef struct DWBBatchHeader { uint32 magic; uint32 version; - uint64 batch_id; /* incarnation id, monotonic in publication - * order within one server run (next_batch_id - * restarts at 1 with each start); the - * apply-pass relies on this to break LSN ties - * between slots of one generation */ + uint64 batch_id; /* incarnation id, assigned at batch open, + * monotonic in open order within one server + * run (next_batch_id restarts at 1 with each + * start); the apply-pass breaks LSN ties + * between slots of one generation by it — a + * deterministic pick, not a strict later-copy + * guarantee: with two writer classes a later + * flush can land in an earlier-opened batch, + * but equal-LSN copies differ only in hint + * bits */ uint32 n_slots; /* capped_slots at seal time */ pg_crc32c crc; /* CRC of all preceding fields */ } DWBBatchHeader; diff --git a/src/test/modules/test_dwb/t/008_modes.pl b/src/test/modules/test_dwb/t/008_modes.pl index 4a313d965b41a..51e92e774e88c 100644 --- a/src/test/modules/test_dwb/t/008_modes.pl +++ b/src/test/modules/test_dwb/t/008_modes.pl @@ -156,6 +156,24 @@ sub wal_window $node->start; $node->stop; +# --- a standing marker suppresses the pass even over a crash ------------- + +# The reverse direction of the marker keying: a crash under an interim +# full_pages run (which touches neither the marker nor the generation) +# must NOT re-arm the ring — an apply here would resurrect ancient +# same-generation slots over pages torn long after the ring was closed. +$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +$node->start; +$node->stop('immediate'); + +$node->append_conf('postgresql.conf', + 'io_torn_pages_protection = double_writes'); +$log_offset = -s $node->logfile; +$node->start; +ok( !$node->log_contains(qr/double write buffer recovery:/, $log_offset), + 'a crash under an interim mode does not re-arm the apply-pass'); +$node->stop; + # --- a corrupt ring control is refused, with a way out ------------------- # Modes that never touch the ring must still refuse an unreadable control @@ -182,4 +200,21 @@ sub wal_window is( $node->safe_psql('postgres', 'SHOW io_torn_pages_protection'), 'full_pages', 'removing pg_dwb unblocks the non-ring mode'); +# --- a crash under "off" is announced on the next protected start -------- + +# Nothing can repair pages torn by a crash that happened while WAL carried +# no images and no ring was active; the restart into a protected mode must +# say so instead of recovering in silence. +$node->append_conf('postgresql.conf', 'io_torn_pages_protection = off'); +$node->restart; +$node->stop('immediate'); + +$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +$log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains( + qr/WARNING: .* database system was interrupted while torn page protection was disabled/, + $log_offset), + 'crash under "off" draws a warning on the protected restart'); + done_testing(); diff --git a/src/test/modules/test_dwb/t/010_recovery.pl b/src/test/modules/test_dwb/t/010_recovery.pl index d72eb37dbf342..5e201a0d77333 100644 --- a/src/test/modules/test_dwb/t/010_recovery.pl +++ b/src/test/modules/test_dwb/t/010_recovery.pl @@ -260,4 +260,36 @@ sub write_block $log_offset), 'a candidate for a dropped relation is counted but skipped'); +# --- an all-zero on-disk page is never repaired -------------------------- + +# Zero the whole block: an empty header means replay recreates the page +# from its init record without reading it, and a stale slot must not +# resurrect on it — the zeroed page's LSN 0 would lose the LSN comparison +# that this skip protects. +$node->safe_psql('postgres', q( + CREATE TABLE tzero AS SELECT g AS id FROM generate_series(1, 100) g; +)); +$node->safe_psql('postgres', 'CHECKPOINT'); +my $tzero_file = + $node->data_dir . '/' + . $node->safe_psql('postgres', "SELECT pg_relation_filepath('tzero')"); +$node->stop('immediate'); + +my $tzero_good = read_block($tzero_file, 0); +write_block($tzero_file, 0, "\0" x 8192); + +$log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains( + qr/double write buffer recovery: 0 of 1 candidate pages restored/, + $log_offset), + 'a zeroed page is not repaired from its slot'); +is( read_block($tzero_file, 0), "\0" x 8192, + '... and stays zero for replay to drive'); + +# put the good page back so the cluster winds down healthy +write_block($tzero_file, 0, $tzero_good); +is( $node->safe_psql('postgres', 'SELECT count(*) FROM tzero'), + '100', 'page manually restored, cluster consistent'); + done_testing(); From 0b64bbd3377294cd21ae25fa4612578f560b2703 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sun, 26 Jul 2026 00:24:29 +0300 Subject: [PATCH 16/52] Reword a comment to avoid a line-wrapped hyphenation --- src/backend/storage/dwb/dwb_recovery.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/storage/dwb/dwb_recovery.c b/src/backend/storage/dwb/dwb_recovery.c index b4e88577e4394..f763ac2f35552 100644 --- a/src/backend/storage/dwb/dwb_recovery.c +++ b/src/backend/storage/dwb/dwb_recovery.c @@ -221,9 +221,9 @@ DWBApplyPass(const DWBControlFileData *control) * hint-bit-only changes does not move the LSN — but such copies * differ only in hint bits, so any of them is a valid redo base * and the id merely makes the pick deterministic; with two writer - * classes a later flush can even land in an earlier- opened - * batch). Within one batch the later slot wins by plain - * overwrite, matching the order the slots were filled in. + * classes a later flush can even land in a batch opened earlier). + * Within one batch the later slot wins by plain overwrite, + * matching the order the slots were filled in. */ n_candidates++; entry = hash_search(candidates, &meta->tag, HASH_ENTER, &found); From d1358ef5a8f88544e34a53d1282ea9418631faf5 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sun, 26 Jul 2026 08:22:38 +0300 Subject: [PATCH 17/52] Trim duplication left by the Stage 4 review rounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DWBReadControlFile duplicated every failure as a LOG-when-tolerated / FATAL-otherwise pair; a single ereport per failure with a variable elevel says the same thing in half the lines. The two restored-backup wipe branches of DWBStartup were byte-identical; hoist one conditional wipe above the mode split. This also discards a shipped ring before the data-checksums check rather than after — an inconsequential reordering, since such a ring is doomed in every mode. The batch_id tie-break story was told in full at three sites, and the last review round had to correct all three in lockstep; the dedup comment in DWBApplyPass keeps the full argument, the two field comments now just point there while retaining the local fact (assignment at batch open, monotonic per run). The raw block I/O helpers were copied into three TAP files; they now live in t/DWBTest.pm (following pg_rewind's RewindTest.pm precedent). --- src/backend/storage/dwb/dwb_file.c | 69 ++++++------------- src/backend/storage/dwb/dwb_recovery.c | 36 ++++------ src/include/storage/dwb.h | 9 +-- src/test/modules/test_dwb/t/005_standby.pl | 26 +------ src/test/modules/test_dwb/t/010_recovery.pl | 26 +------ .../test_dwb/t/011_geometry_recovery.pl | 26 +------ src/test/modules/test_dwb/t/DWBTest.pm | 38 ++++++++++ 7 files changed, 84 insertions(+), 146 deletions(-) create mode 100644 src/test/modules/test_dwb/t/DWBTest.pm diff --git a/src/backend/storage/dwb/dwb_file.c b/src/backend/storage/dwb/dwb_file.c index 663c161e33505..b10d36433f07a 100644 --- a/src/backend/storage/dwb/dwb_file.c +++ b/src/backend/storage/dwb/dwb_file.c @@ -103,6 +103,8 @@ bool DWBReadControlFile(DWBControlFileData *control, bool missing_ok, bool *corruptp) { + /* with elevel < ERROR the ereports return and the *corruptp tails run */ + int elevel = corruptp ? LOG : FATAL; int fd; int r; @@ -114,18 +116,11 @@ DWBReadControlFile(DWBControlFileData *control, bool missing_ok, { if (errno == ENOENT && missing_ok) return false; - if (corruptp) - { - ereport(LOG, - (errcode_for_file_access(), - errmsg("could not open file \"%s\": %m", - DWB_CONTROL_FILE))); - *corruptp = true; - return false; - } - ereport(FATAL, + ereport(elevel, (errcode_for_file_access(), errmsg("could not open file \"%s\": %m", DWB_CONTROL_FILE))); + *corruptp = true; + return false; } pgstat_report_wait_start(WAIT_EVENT_DWB_CONTROL_READ); @@ -134,38 +129,25 @@ DWBReadControlFile(DWBControlFileData *control, bool missing_ok, pgstat_report_wait_end(); if (r != sizeof(DWBControlFileData)) { - if (corruptp) - { - /* distinguish a real read error from a truncated file */ - if (r < 0) - ereport(LOG, - (errcode_for_file_access(), - errmsg("could not read file \"%s\": %m", - DWB_CONTROL_FILE))); - else - ereport(LOG, - (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read file \"%s\": read %d of %zu", - DWB_CONTROL_FILE, r, - sizeof(DWBControlFileData)))); - if (CloseTransientFile(fd) != 0) - ereport(LOG, - (errcode_for_file_access(), - errmsg("could not close file \"%s\": %m", - DWB_CONTROL_FILE))); - *corruptp = true; - return false; - } /* distinguish a real read error from a truncated file */ if (r < 0) - ereport(FATAL, + ereport(elevel, (errcode_for_file_access(), errmsg("could not read file \"%s\": %m", DWB_CONTROL_FILE))); - ereport(FATAL, - (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read file \"%s\": read %d of %zu", - DWB_CONTROL_FILE, r, sizeof(DWBControlFileData)))); + else + ereport(elevel, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("could not read file \"%s\": read %d of %zu", + DWB_CONTROL_FILE, r, + sizeof(DWBControlFileData)))); + if (CloseTransientFile(fd) != 0) + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", + DWB_CONTROL_FILE))); + *corruptp = true; + return false; } if (CloseTransientFile(fd) != 0) ereport(FATAL, @@ -175,19 +157,12 @@ DWBReadControlFile(DWBControlFileData *control, bool missing_ok, if (control->magic != DWB_CONTROL_MAGIC || !EQ_CRC32C(control->crc, DWBControlCrc(control))) { - if (corruptp) - { - ereport(LOG, - (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("invalid checksum or magic number in file \"%s\"", - DWB_CONTROL_FILE))); - *corruptp = true; - return false; - } - ereport(FATAL, + ereport(elevel, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("invalid checksum or magic number in file \"%s\"", DWB_CONTROL_FILE))); + *corruptp = true; + return false; } if (control->min_version > DWB_VERSION) ereport(FATAL, diff --git a/src/backend/storage/dwb/dwb_recovery.c b/src/backend/storage/dwb/dwb_recovery.c index f763ac2f35552..bdc7df7c2909e 100644 --- a/src/backend/storage/dwb/dwb_recovery.c +++ b/src/backend/storage/dwb/dwb_recovery.c @@ -51,12 +51,8 @@ typedef struct DWBApplyCandidate { BufferTag tag; /* hash key */ XLogRecPtr lsn; - uint64 batch_id; /* tie-breaker for equal LSNs: equal-LSN - * copies of one generation can differ only in - * hint bits, so either is a valid redo base - * and the id (monotonic in batch-open order, - * see DWBBatchHeader.batch_id) just makes the - * pick deterministic */ + uint64 batch_id; /* LSN tie-breaker; see the dedup comment in + * the scan loop below */ uint32 batch_idx; uint32 slot_idx; pg_crc32c image_crc; /* revalidates the image on re-read */ @@ -462,6 +458,17 @@ DWBStartup(bool restoring_backup) bool corrupt; bool need_apply; + /* + * A ring shipped inside a restored backup must not survive in any mode: + * dormant, it would greet a much later switch to double_writes with a + * plausible control file, and under double_writes it must not be applied + * (see the header comment). Discard it before anything reads the ring + * state. + */ + if (restoring_backup && DWBWipeRing()) + ereport(LOG, + (errmsg("discarding double write buffer ring contents restored from a base backup"))); + if (!DWBIsEnabled()) { /* @@ -475,17 +482,7 @@ DWBStartup(bool restoring_backup) errdetail("WAL carries no full page images; \"full_page_writes\" is ignored in this mode."))); if (restoring_backup) - { - /* - * A foreign ring shipped in a restored backup is dangerous even - * lying dormant: a much later switch to double_writes would find - * it with a plausible control file. Discard it now. - */ - if (DWBWipeRing()) - ereport(LOG, - (errmsg("discarding double write buffer ring contents restored from a base backup"))); return InvalidXLogRecPtr; - } /* * Mode-downgrade guard: a ring that was not cleanly closed may hold @@ -534,13 +531,6 @@ DWBStartup(bool restoring_backup) errmsg("io_torn_pages_protection = \"double_writes\" requires data checksums"), errhint("Enable checksums with initdb -k or pg_checksums."))); - if (restoring_backup) - { - if (DWBWipeRing()) - ereport(LOG, - (errmsg("discarding double write buffer ring contents restored from a base backup"))); - } - if (!DWBReadControlFile(&control, true, NULL)) { /* diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index d14f4b9344972..8b6c7957d530e 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -143,13 +143,8 @@ typedef struct DWBBatchHeader uint64 batch_id; /* incarnation id, assigned at batch open, * monotonic in open order within one server * run (next_batch_id restarts at 1 with each - * start); the apply-pass breaks LSN ties - * between slots of one generation by it — a - * deterministic pick, not a strict later-copy - * guarantee: with two writer classes a later - * flush can land in an earlier-opened batch, - * but equal-LSN copies differ only in hint - * bits */ + * start); the apply-pass dedup uses it as an + * LSN tie-breaker — see DWBApplyPass */ uint32 n_slots; /* capped_slots at seal time */ pg_crc32c crc; /* CRC of all preceding fields */ } DWBBatchHeader; diff --git a/src/test/modules/test_dwb/t/005_standby.pl b/src/test/modules/test_dwb/t/005_standby.pl index 7d274f2fd6dca..2eded44aec3e8 100644 --- a/src/test/modules/test_dwb/t/005_standby.pl +++ b/src/test/modules/test_dwb/t/005_standby.pl @@ -9,6 +9,9 @@ use strict; use warnings FATAL => 'all'; +use FindBin; +use lib $FindBin::RealBin; +use DWBTest; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; @@ -161,29 +164,6 @@ # DB_IN_ARCHIVE_RECOVERY — the branch that may raise minRecoveryPoint — # and must repair from the standby's OWN ring: the replayed WAL carries no # page images that could do it instead. -sub read_block -{ - my ($file, $blkno) = @_; - my $buf; - - open my $fh, '<:raw', $file or die "could not open $file: $!"; - sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; - sysread($fh, $buf, 8192) == 8192 or die "short read from $file: $!"; - close $fh; - return $buf; -} - -sub write_block -{ - my ($file, $blkno, $buf) = @_; - - open my $fh, '+<:raw', $file or die "could not open $file: $!"; - sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; - syswrite($fh, $buf) == length($buf) or die "short write to $file: $!"; - close $fh; - return; -} - $primary->safe_psql('postgres', q( CREATE TABLE ts_repair AS SELECT g AS id FROM generate_series(1, 100) g; )); diff --git a/src/test/modules/test_dwb/t/010_recovery.pl b/src/test/modules/test_dwb/t/010_recovery.pl index 5e201a0d77333..038b084f627e1 100644 --- a/src/test/modules/test_dwb/t/010_recovery.pl +++ b/src/test/modules/test_dwb/t/010_recovery.pl @@ -21,6 +21,9 @@ use strict; use warnings FATAL => 'all'; use File::Path qw(rmtree); +use FindBin; +use lib $FindBin::RealBin; +use DWBTest; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::RecursiveCopy; use PostgreSQL::Test::Utils; @@ -41,29 +44,6 @@ )); $node->start; -sub read_block -{ - my ($file, $blkno) = @_; - my $buf; - - open my $fh, '<:raw', $file or die "could not open $file: $!"; - sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; - sysread($fh, $buf, 8192) == 8192 or die "short read from $file: $!"; - close $fh; - return $buf; -} - -sub write_block -{ - my ($file, $blkno, $buf) = @_; - - open my $fh, '+<:raw', $file or die "could not open $file: $!"; - sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; - syswrite($fh, $buf) == length($buf) or die "short write to $file: $!"; - close $fh; - return; -} - $node->safe_psql('postgres', q( CREATE TABLE thint AS SELECT g AS id FROM generate_series(1, 100) g; CREATE TABLE told AS SELECT g AS id FROM generate_series(1, 100) g; diff --git a/src/test/modules/test_dwb/t/011_geometry_recovery.pl b/src/test/modules/test_dwb/t/011_geometry_recovery.pl index 1e7693c71601b..cf4c96af7aa07 100644 --- a/src/test/modules/test_dwb/t/011_geometry_recovery.pl +++ b/src/test/modules/test_dwb/t/011_geometry_recovery.pl @@ -16,6 +16,9 @@ use strict; use warnings FATAL => 'all'; +use FindBin; +use lib $FindBin::RealBin; +use DWBTest; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; @@ -34,29 +37,6 @@ )); $node->start; -sub read_block -{ - my ($file, $blkno) = @_; - my $buf; - - open my $fh, '<:raw', $file or die "could not open $file: $!"; - sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; - sysread($fh, $buf, 8192) == 8192 or die "short read from $file: $!"; - close $fh; - return $buf; -} - -sub write_block -{ - my ($file, $blkno, $buf) = @_; - - open my $fh, '+<:raw', $file or die "could not open $file: $!"; - sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; - syswrite($fh, $buf) == length($buf) or die "short write to $file: $!"; - close $fh; - return; -} - $node->safe_psql('postgres', q( CREATE TABLE tgeo AS SELECT g AS id FROM generate_series(1, 100) g; )); diff --git a/src/test/modules/test_dwb/t/DWBTest.pm b/src/test/modules/test_dwb/t/DWBTest.pm new file mode 100644 index 0000000000000..614f89091ecde --- /dev/null +++ b/src/test/modules/test_dwb/t/DWBTest.pm @@ -0,0 +1,38 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Shared helpers for the test_dwb TAP suite: raw 8 kB block I/O on relation +# files, for damaging and inspecting pages behind the server's back. + +package DWBTest; + +use strict; +use warnings FATAL => 'all'; +use Exporter 'import'; + +our @EXPORT = qw(read_block write_block); + +sub read_block +{ + my ($file, $blkno) = @_; + my $buf; + + open my $fh, '<:raw', $file or die "could not open $file: $!"; + sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; + sysread($fh, $buf, 8192) == 8192 or die "short read from $file: $!"; + close $fh; + return $buf; +} + +sub write_block +{ + my ($file, $blkno, $buf) = @_; + + open my $fh, '+<:raw', $file or die "could not open $file: $!"; + sysseek($fh, $blkno * 8192, 0) or die "could not seek $file: $!"; + syswrite($fh, $buf) == length($buf) or die "short write to $file: $!"; + close $fh; + return; +} + +1; From 96716b7e82e994875ff3c28997b5b764a9a141c1 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sun, 26 Jul 2026 09:24:48 +0300 Subject: [PATCH 18/52] Cover the deferred recovery paths and pg_upgrade (Stage 5) Pin down the apply-pass behaviours the runtime write path cannot be steered into: * New test_dwb_craft_batch() writes a synthetic single-slot batch file directly, taking geometry and generation from the ring control; the image is the block's on-disk content with a chosen LSN, a marker in the page hole and a recomputed checksum, so an applied image remains a valid page. 012_apply_crafted.pl uses it to pin the dedup comparator - the higher LSN wins, and an equal-LSN tie goes to the higher batch_id against scan order - and the standby minRecoveryPoint raise, crafting a slot LSN inside the received-but-unreplayed window held open by pg_wal_replay_pause. * 013_backup_start_point.pl pins the backupStartPoint arm of the restored-backup detection end to end: a low-level copy taken inside an open backup window lacks the backup-end WAL, so recovery from it fails deterministically; the first start consumes backup_label, the second start runs on backupStartPoint alone, and both must discard the ring instead of applying it. * 014_pg_upgrade.pl runs a same-version pg_upgrade from a double_writes cluster: nothing of pg_dwb/ transfers, and the upgraded cluster cold-starts a ring of its own. * New test_dwb_set_control_min_version() rewrites the ring control with a chosen min_version and a matching CRC; 008_modes.pl asserts that the format-version refusal names the version gap rather than the corrupt-ring removal advice, and that leftovers of an interrupted wipe (batch files behind a missing control) are swept by the cold-create path. * 009_fpw_transition.pl asserts that a crashed double_writes standby of an "off" primary restarts without the crash-under-off warning, and 006_backup.pl that an ordinary restore does not claim to discard the empty pg_dwb directory it ships. --- src/test/modules/test_dwb/.gitignore | 4 + src/test/modules/test_dwb/meson.build | 3 + src/test/modules/test_dwb/t/006_backup.pl | 4 + src/test/modules/test_dwb/t/008_modes.pl | 56 +++++++ .../modules/test_dwb/t/009_fpw_transition.pl | 23 +++ .../modules/test_dwb/t/012_apply_crafted.pl | 158 ++++++++++++++++++ .../test_dwb/t/013_backup_start_point.pl | 100 +++++++++++ src/test/modules/test_dwb/t/014_pg_upgrade.pl | 70 ++++++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 9 + src/test/modules/test_dwb/test_dwb.c | 142 ++++++++++++++++ 10 files changed, 569 insertions(+) create mode 100644 src/test/modules/test_dwb/t/012_apply_crafted.pl create mode 100644 src/test/modules/test_dwb/t/013_backup_start_point.pl create mode 100644 src/test/modules/test_dwb/t/014_pg_upgrade.pl diff --git a/src/test/modules/test_dwb/.gitignore b/src/test/modules/test_dwb/.gitignore index 5dcb3ff972350..4c5757d966fad 100644 --- a/src/test/modules/test_dwb/.gitignore +++ b/src/test/modules/test_dwb/.gitignore @@ -2,3 +2,7 @@ /log/ /results/ /tmp_check/ + +# Generated by the pg_upgrade run in t/014_pg_upgrade.pl +/delete_old_cluster.sh +/delete_old_cluster.bat diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index 8aeec8c37c2ac..a9074801fdcfb 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -48,6 +48,9 @@ tests += { 't/009_fpw_transition.pl', 't/010_recovery.pl', 't/011_geometry_recovery.pl', + 't/012_apply_crafted.pl', + 't/013_backup_start_point.pl', + 't/014_pg_upgrade.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/006_backup.pl b/src/test/modules/test_dwb/t/006_backup.pl index 65940677fee61..6a174619acf98 100644 --- a/src/test/modules/test_dwb/t/006_backup.pl +++ b/src/test/modules/test_dwb/t/006_backup.pl @@ -118,6 +118,10 @@ qr/double write buffer ring opened: 16 batches of 16 pages, generation 1\b/, $restored_log_offset), 'restored cluster opened a fresh ring'); +ok( !$restored->log_contains( + qr/discarding double write buffer ring contents/, + $restored_log_offset), + '... without claiming to discard the empty restored pg_dwb'); is( $restored->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpi'), '100', 'restored data is intact'); $restored->stop; diff --git a/src/test/modules/test_dwb/t/008_modes.pl b/src/test/modules/test_dwb/t/008_modes.pl index 51e92e774e88c..9462279f5d2dd 100644 --- a/src/test/modules/test_dwb/t/008_modes.pl +++ b/src/test/modules/test_dwb/t/008_modes.pl @@ -217,4 +217,60 @@ sub wal_window $log_offset), 'crash under "off" draws a warning on the protected restart'); +# --- a ring of a newer format version is refused intact ------------------ + +# A binary downgrade can meet a ring whose min_version exceeds what this +# server reads. That ring is intact and may hold unapplied repairs only +# the newer server understands, so the refusal must name the version gap — +# never the "corrupt, remove pg_dwb" advice, which would invite discarding +# it. +$node->append_conf('postgresql.conf', + 'io_torn_pages_protection = double_writes'); +$node->restart; +$node->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); +$node->safe_psql('postgres', 'SELECT test_dwb_set_control_min_version(2)'); +# an immediate stop: a clean shutdown would try to rewrite the control +$node->stop('immediate'); + +$log_offset = -s $node->logfile; +$ret = $node->start(fail_ok => 1); +is($ret, 0, 'a too-new ring format refuses the start'); +ok( $node->log_contains( + qr!FATAL: .* file "pg_dwb/control" requires format version at least 2, but this server supports 1!, + $log_offset), + '... naming the version gap'); +ok( !$node->log_contains(qr/could not be validated/, $log_offset), + '... and not the corrupt-ring advice'); + +# the intact-but-unreadable ring can only be resolved by removal +rmtree($node->data_dir . '/pg_dwb'); +$log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains( + qr/ring opened: .* generation 1\b/, $log_offset), + 'removing the newer ring unblocks a fresh double_writes start'); + +# --- leftovers of an interrupted wipe are swept, not fatal --------------- + +# DWBWipeRing removes the control first, durably: a crash between that and +# the batch sweep leaves batch files behind a missing control. The next +# start must take the cold-create path and clear them — a start that +# trusted the batch files would fail on the missing control forever. +$node->stop; +unlink($node->data_dir . '/pg_dwb/control') + or die "unlink pg_dwb/control: $!"; +my $leftover = $node->data_dir . '/pg_dwb/batch_9999'; +open my $lf, '>', $leftover or die "open $leftover: $!"; +print $lf 'leftover of an interrupted wipe'; +close $lf; + +$log_offset = -s $node->logfile; +$node->start; +ok( !$node->log_contains(qr/double write buffer recovery:/, $log_offset), + 'no apply-pass over the swept leftovers'); +ok( $node->log_contains( + qr/ring opened: .* generation 1\b/, $log_offset), + 'the interrupted-wipe state cold-starts a fresh ring'); +ok(!-e $leftover, 'the leftover batch file is gone'); + done_testing(); diff --git a/src/test/modules/test_dwb/t/009_fpw_transition.pl b/src/test/modules/test_dwb/t/009_fpw_transition.pl index 225cb33677ff3..9f189d71a5722 100644 --- a/src/test/modules/test_dwb/t/009_fpw_transition.pl +++ b/src/test/modules/test_dwb/t/009_fpw_transition.pl @@ -122,4 +122,27 @@ is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpw'), '3000', 'and replays the image-less WAL'); +# --- a crash of a double_writes standby of an "off" primary is quiet ------ + +# The crash-under-off warning keys on pg_control's io_torn_pages_protection, +# which on a standby describes the PRIMARY's run, not the one that crashed +# here. A double_writes standby of an "off" primary repairs its own torn +# pages from its own ring, so its crash restart must stay silent. +$primary->append_conf('postgresql.conf', 'io_torn_pages_protection = off'); +$primary->restart; +$primary->safe_psql('postgres', + 'INSERT INTO dwb_fpw SELECT g FROM generate_series(3001, 4000) g'); +$primary->wait_for_catchup($standby); + +$standby->stop('immediate'); +my $warn_offset = -s $standby->logfile; +$standby->start; +ok( !$standby->log_contains( + qr/interrupted while torn page protection was disabled/, + $warn_offset), + 'crashed double_writes standby of an "off" primary draws no warning'); +$primary->wait_for_catchup($standby); +is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpw'), + '4000', 'and keeps replaying'); + done_testing(); diff --git a/src/test/modules/test_dwb/t/012_apply_crafted.pl b/src/test/modules/test_dwb/t/012_apply_crafted.pl new file mode 100644 index 0000000000000..b24246a9c19d3 --- /dev/null +++ b/src/test/modules/test_dwb/t/012_apply_crafted.pl @@ -0,0 +1,158 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# The apply-pass dedup comparator, exercised through crafted batch files. +# The runtime write path cannot produce two surviving copies of one page — +# with dwb_retire_workers = 0 a sequential writer keeps reusing the lowest +# ring index — so test_dwb_craft_batch() writes the competing candidates +# directly: same page, two batches, chosen LSNs and batch_ids. The DEBUG1 +# "restoring ... from batch N" line names the winning slot exactly. +# +# The same helper drives the standby scenario: a crafted slot whose LSN lies +# beyond the standby's minRecoveryPoint must make the startup apply-pass +# raise it before consistency can be declared. + +use strict; +use warnings FATAL => 'all'; +use FindBin; +use lib $FindBin::RealBin; +use DWBTest; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $primary = PostgreSQL::Test::Cluster->new('dwb_craft'); +$primary->init(allows_streaming => 1); +$primary->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 0 +dwb_batch_timeout_ms = 20 +autovacuum = off +bgwriter_lru_maxpages = 0 +log_min_messages = debug1 +)); +$primary->start; +$primary->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); + +$primary->safe_psql('postgres', q( + CREATE TABLE tlsn AS SELECT g AS id FROM generate_series(1, 100) g; + CREATE TABLE ttie AS SELECT g AS id FROM generate_series(1, 100) g; +)); +$primary->safe_psql('postgres', 'CHECKPOINT'); + +my $tlsn_file = + $primary->data_dir . '/' + . $primary->safe_psql('postgres', "SELECT pg_relation_filepath('tlsn')"); +my $ttie_file = + $primary->data_dir . '/' + . $primary->safe_psql('postgres', "SELECT pg_relation_filepath('ttie')"); +my $tlsn_relnum = $primary->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE relname = 'tlsn'"); +my $ttie_relnum = $primary->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE relname = 'ttie'"); + +# --- higher LSN wins the dedup; on a tie the higher batch_id does --------- + +# All crafted LSNs must stay at or below the real WAL insert position (they +# become page LSNs of live pages) and above the blocks' current disk LSNs: +# pad the WAL a little and take LSNs from just below the insert position. +$primary->safe_psql('postgres', + "SELECT pg_logical_emit_message(false, 'dwb', repeat('x', 256))") + for (1 .. 2); +my $insert_lsn = + $primary->safe_psql('postgres', 'SELECT pg_current_wal_insert_lsn()'); +my ($lsn_a, $lsn_b) = split /\|/, + $primary->safe_psql('postgres', + "SELECT '$insert_lsn'::pg_lsn - 16, '$insert_lsn'::pg_lsn - 8"); + +# The LSN pair goes into batches 14/15. The tie pair goes into 12/13 with +# the HIGHER batch_id in the LOWER batch index, so a comparator that merely +# kept the later-scanned candidate would pick the wrong slot. +$primary->safe_psql('postgres', qq( + SELECT test_dwb_craft_batch(14, 501, $tlsn_relnum, 0, '$lsn_a', 'DWBLSNLOSER'); + SELECT test_dwb_craft_batch(15, 502, $tlsn_relnum, 0, '$lsn_b', 'DWBLSNWINNER'); + SELECT test_dwb_craft_batch(12, 601, $ttie_relnum, 0, '$insert_lsn', 'DWBTIEWINNER'); + SELECT test_dwb_craft_batch(13, 600, $ttie_relnum, 0, '$insert_lsn', 'DWBTIELOSER'); +)); +$primary->stop('immediate'); + +my $log_offset = -s $primary->logfile; +$primary->start; +ok( $primary->log_contains( + qr!restoring page 0 of relation \d+/\d+/$tlsn_relnum fork 0 from batch 15!, + $log_offset), + 'the higher-LSN copy won the dedup'); +ok( $primary->log_contains( + qr!restoring page 0 of relation \d+/\d+/$ttie_relnum fork 0 from batch 12!, + $log_offset), + 'on equal LSNs the higher batch_id won, against scan order'); + +like(read_block($tlsn_file, 0), qr/DWBLSNWINNER/, + 'winning image is on disk'); +unlike(read_block($tlsn_file, 0), qr/DWBLSNLOSER/, + '... and the losing image is not'); +like(read_block($ttie_file, 0), qr/DWBTIEWINNER/, + 'winning tie image is on disk'); +unlike(read_block($ttie_file, 0), qr/DWBTIELOSER/, + '... and the losing tie image is not'); + +is( $primary->safe_psql( + 'postgres', 'SELECT count(*) FROM tlsn UNION ALL SELECT count(*) FROM ttie'), + "100\n100", 'both repaired pages read back fine'); + +# --- a crafted slot beyond minRecoveryPoint raises it on the standby ------ + +$primary->backup('bkp'); +my $standby = PostgreSQL::Test::Cluster->new('dwb_craft_standby'); +$standby->init_from_backup($primary, 'bkp', has_streaming => 1); +$standby->start; + +$primary->safe_psql('postgres', q( + CREATE TABLE tmrp AS SELECT g AS id FROM generate_series(1, 100) g; +)); +$primary->safe_psql('postgres', 'CHECKPOINT'); +$primary->wait_for_catchup($standby); +# the restartpoint flushes tmrp's block to the standby's disk +$standby->safe_psql('postgres', 'CHECKPOINT'); +my $tmrp_relnum = $standby->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE relname = 'tmrp'"); +my $tmrp_file = + $standby->data_dir . '/' + . $standby->safe_psql('postgres', "SELECT pg_relation_filepath('tmrp')"); + +# Hold replay while the primary moves ahead: the standby then holds +# received-but-unreplayed WAL, and any LSN inside it lies beyond the +# standby's minRecoveryPoint yet within the WAL it can replay to. +$standby->safe_psql('postgres', 'SELECT pg_wal_replay_pause()'); +$primary->safe_psql('postgres', + "SELECT pg_logical_emit_message(false, 'dwb', repeat('x', 1024))"); +my $mrp_lsn = + $primary->safe_psql('postgres', 'SELECT pg_current_wal_insert_lsn()'); +$primary->wait_for_catchup($standby, 'flush', $mrp_lsn); + +$standby->safe_psql('postgres', + "SELECT test_dwb_craft_batch(15, 700, $tmrp_relnum, 0, '$mrp_lsn', 'DWBMRPMARK')"); +$standby->stop('immediate'); + +$log_offset = -s $standby->logfile; +$standby->start; +ok( $standby->log_contains( + qr!restoring page 0 of relation \d+/\d+/$tmrp_relnum fork 0 from batch 15!, + $log_offset), + 'crafted slot applied on the standby'); +my $mrp_re = quotemeta($mrp_lsn); +ok( $standby->log_contains( + qr/raising minimum recovery point to $mrp_re to cover pages repaired from the double write buffer/, + $log_offset), + 'minimum recovery point raised to the applied LSN'); +like(read_block($tmrp_file, 0), qr/DWBMRPMARK/, + 'crafted image is on the standby disk'); + +$primary->wait_for_catchup($standby); +is( $standby->safe_psql('postgres', 'SELECT count(*) FROM tmrp'), + '100', 'standby reads the repaired page fine'); + +done_testing(); diff --git a/src/test/modules/test_dwb/t/013_backup_start_point.pl b/src/test/modules/test_dwb/t/013_backup_start_point.pl new file mode 100644 index 0000000000000..cc797f2127574 --- /dev/null +++ b/src/test/modules/test_dwb/t/013_backup_start_point.pl @@ -0,0 +1,100 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# The second arm of the restored-backup detection: backup_label is renamed +# away early in the first recovery attempt, so a crash mid-backup-recovery +# leaves only pg_control's backupStartPoint to say "this is still a restored +# backup". A low-level backup copied without the backup-end WAL fails its +# recovery deterministically ("WAL ends before end of online backup"), which +# yields both states in turn: the first start sees the label, the second +# start sees no label but a set backupStartPoint — and both must discard +# the ring instead of applying it. + +use strict; +use warnings FATAL => 'all'; +use FindBin; +use lib $FindBin::RealBin; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::RecursiveCopy; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_bsp'); +$node->init(allows_streaming => 1); +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 0 +dwb_batch_timeout_ms = 20 +autovacuum = off +)); +$node->start; + +$node->safe_psql('postgres', q( + CREATE TABLE dwb_bsp AS SELECT g AS id FROM generate_series(1, 100) g; +)); + +# A low-level backup: open the window, OS-copy the whole data directory, +# close the window. The copy predates pg_backup_stop, so it cannot contain +# the backup-end WAL record — recovery from it must run out of WAL inside +# the backup window. +my $bk = $node->background_psql('postgres'); +$bk->query_safe('SET client_min_messages = warning'); +$bk->query_safe("SELECT pg_backup_start('dwb_bsp_wide', true)"); +$node->safe_psql('postgres', + 'INSERT INTO dwb_bsp SELECT g FROM generate_series(101, 200) g'); + +my $backup_path = $node->backup_dir . '/wide'; +PostgreSQL::Test::RecursiveCopy::copypath($node->data_dir, $backup_path); +unlink("$backup_path/postmaster.pid", "$backup_path/postmaster.opts"); + +my $label = $bk->query_safe('SELECT labelfile FROM pg_backup_stop()'); +$bk->quit; +like($label, qr/^START WAL LOCATION/, 'pg_backup_stop returned the label'); +open my $lf, '>', "$backup_path/backup_label" or die "backup_label: $!"; +print $lf $label; +print $lf "\n" unless $label =~ /\n$/; +close $lf; + +my $restored = PostgreSQL::Test::Cluster->new('dwb_bsp_restored'); +$restored->init_from_backup($node, 'wide'); +my $pgdata = $restored->data_dir; + +# --- first start: the label arm ------------------------------------------ + +my $log_offset = -s $restored->logfile; +my $ret = $restored->start(fail_ok => 1); +is($ret, 0, 'recovery without the backup-end WAL fails'); +ok( $restored->log_contains( + qr/FATAL: .* WAL ends before end of online backup/, $log_offset), + '... for the expected reason'); +ok( $restored->log_contains( + qr/discarding double write buffer ring contents restored from a base backup/, + $log_offset), + 'the label start discarded the copied ring'); + +ok(!-f "$pgdata/backup_label", 'the failed recovery consumed backup_label'); +ok(-f "$pgdata/backup_label.old", '... renaming it out of the way'); +my ($cd, $cderr) = run_command([ 'pg_controldata', $pgdata ]); +like($cd, qr/Backup start location:\s+(?!0\/0)\S/, + 'pg_control still carries backupStartPoint'); + +# --- second start: the backupStartPoint arm ------------------------------ + +# No label anymore; only the control flag says this recovery still belongs +# to a restored backup. The ring recreated by the first start must be +# discarded again, not applied. +$log_offset = -s $restored->logfile; +$ret = $restored->start(fail_ok => 1); +is($ret, 0, 'the second recovery attempt fails the same way'); +ok( $restored->log_contains( + qr/discarding double write buffer ring contents restored from a base backup/, + $log_offset), + 'backupStartPoint alone still discards the ring'); +ok( !$restored->log_contains( + qr/double write buffer recovery:/, $log_offset), + '... and no apply-pass ran on either start'); + +done_testing(); diff --git a/src/test/modules/test_dwb/t/014_pg_upgrade.pl b/src/test/modules/test_dwb/t/014_pg_upgrade.pl new file mode 100644 index 0000000000000..ca827270df18f --- /dev/null +++ b/src/test/modules/test_dwb/t/014_pg_upgrade.pl @@ -0,0 +1,70 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# pg_upgrade and the ring: the new cluster is a fresh initdb, so nothing of +# pg_dwb/ ever transfers — the old ring stays with the old cluster, and the +# upgraded cluster cold-starts a ring of its own on its first double_writes +# start. A same-version upgrade exercises the whole path, including +# pg_upgrade's own starts of the cleanly-stopped double_writes old cluster. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $dwb_conf = qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 0 +dwb_batch_timeout_ms = 20 +autovacuum = off +); + +my $old = PostgreSQL::Test::Cluster->new('dwb_upgrade_old'); +$old->init; +$old->append_conf('postgresql.conf', $dwb_conf); +$old->start; +$old->safe_psql('postgres', q( + CREATE TABLE dwb_up AS SELECT g AS id FROM generate_series(1, 100) g; +)); +$old->safe_psql('postgres', 'CHECKPOINT'); +$old->stop; +ok(-f $old->data_dir . '/pg_dwb/control', + 'the old cluster leaves a ring behind'); + +my $new = PostgreSQL::Test::Cluster->new('dwb_upgrade_new'); +$new->init; + +my $bindir = $new->config_data('--bindir'); +command_ok( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $old->data_dir, + '--new-datadir' => $new->data_dir, + '--old-bindir' => $bindir, + '--new-bindir' => $bindir, + '--socketdir' => $new->host, + '--old-port' => $old->port, + '--new-port' => $new->port, + ], + 'pg_upgrade from a double_writes cluster succeeds'); + +ok(-f $old->data_dir . '/pg_dwb/control', + 'the old ring stays with the old cluster'); +ok(!-d $new->data_dir . '/pg_dwb', + 'nothing of the ring was shipped into the new cluster'); + +# the upgraded cluster starts its double_writes life cold +$new->append_conf('postgresql.conf', $dwb_conf); +my $log_offset = -s $new->logfile; +$new->start; +ok( $new->log_contains( + qr/double write buffer ring opened: 16 batches of 16 pages, generation 1\b/, + $log_offset), + 'the upgraded cluster cold-starts a fresh ring'); +is( $new->safe_psql('postgres', 'SELECT count(*) FROM dwb_up'), + '100', 'the upgraded data is intact'); + +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index be082371a4221..465e34b494d68 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -74,3 +74,12 @@ CREATE FUNCTION test_dwb_park(relnumber oid) CREATE FUNCTION test_dwb_stale_snapshot(relnumber oid) RETURNS void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_craft_batch(batch_idx int, batch_id int8, + relnumber oid, blkno int, lsn pg_lsn, marker text) + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_set_control_min_version(min_version int) + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index 8ea81b413495b..e83948d0378c7 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -24,11 +24,14 @@ #include "fmgr.h" #include "miscadmin.h" #include "storage/bufpage.h" +#include "storage/checksum.h" #include "storage/dwb.h" #include "storage/fd.h" #include "storage/smgr.h" #include "storage/sync.h" #include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "varatt.h" PG_MODULE_MAGIC; @@ -794,3 +797,142 @@ test_dwb_open_stale(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } + +/* + * Write a synthetic single-slot batch file directly into pg_dwb/, bypassing + * the ring state machine. The apply-pass dedup scenarios need on-disk + * layouts — the same page in two batches with chosen LSNs and batch_ids — + * that the runtime write path cannot be steered into: a sequential writer + * keeps reusing the lowest free ring index, so only the last copy of a page + * survives on disk. + * + * The image is the target block's current on-disk content with the given + * LSN, the marker planted in the page hole and the checksum recomputed, so + * an applied image is a valid page the server can read back afterwards. + * The caller keeps the batch index away from runtime traffic (quiet server, + * high index) and must not hand out an LSN beyond the current WAL insert + * position: it ends up as a real page LSN, and a later flush of that page + * would ask XLogFlush for WAL that does not exist. + */ +PG_FUNCTION_INFO_V1(test_dwb_craft_batch); +Datum +test_dwb_craft_batch(PG_FUNCTION_ARGS) +{ + int batch_idx = PG_GETARG_INT32(0); + uint64 batch_id = (uint64) PG_GETARG_INT64(1); + Oid relnumber = PG_GETARG_OID(2); + BlockNumber blkno = (BlockNumber) PG_GETARG_INT32(3); + XLogRecPtr lsn = PG_GETARG_LSN(4); + text *marker = PG_GETARG_TEXT_PP(5); + BufferTag tag = make_tag(MyDatabaseId, relnumber, blkno); + DWBControlFileData control; + DWBBatchHeader hdr; + DWSlotMeta meta; + static PGAlignedBlock image; + PageHeader ph = (PageHeader) image.data; + RelPathStr relpath; + char path[MAXPGPATH]; + char *region; + Size region_size; + int fd; + + check_dwb_enabled(); + + if (!DWBReadControlFile(&control, false, NULL)) + pg_unreachable(); + if (batch_idx < 0 || (uint32) batch_idx >= control.num_batches) + ereport(ERROR, (errmsg("batch index out of range"))); + + /* base image: the block's current on-disk content */ + relpath = relpathperm(BufTagGetRelFileLocator(&tag), MAIN_FORKNUM); + fd = OpenTransientFile(relpath.str, O_RDONLY | PG_BINARY); + if (fd < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", relpath.str))); + errno = 0; + if (pg_pread(fd, image.data, BLCKSZ, (off_t) blkno * BLCKSZ) != BLCKSZ) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read block %u of file \"%s\": %m", + blkno, relpath.str))); + if (CloseTransientFile(fd) != 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", relpath.str))); + + if (PageIsNew((Page) image.data)) + ereport(ERROR, + (errmsg("block %u of \"%s\" is empty on disk; CHECKPOINT first", + blkno, relpath.str))); + if ((Size) (ph->pd_upper - ph->pd_lower) < VARSIZE_ANY_EXHDR(marker)) + ereport(ERROR, (errmsg("marker does not fit into the page hole"))); + + memcpy(image.data + ph->pd_lower, VARDATA_ANY(marker), + VARSIZE_ANY_EXHDR(marker)); + PageSetLSN((Page) image.data, lsn); + ph->pd_checksum = pg_checksum_page(image.data, blkno); + + memset(&meta, 0, sizeof(meta)); + meta.tag = tag; + meta.page_lsn = lsn; + meta.generation = control.generation; + meta.image_crc = DWBImageCrc(image.data); + meta.meta_crc = DWBSlotMetaCrc(&meta); + + memset(&hdr, 0, sizeof(hdr)); + hdr.magic = DWB_BATCH_MAGIC; + hdr.version = DWB_VERSION; + hdr.batch_id = batch_id; + hdr.n_slots = 1; + hdr.crc = DWBBatchHeaderCrc(&hdr); + + region_size = DWBMetaRegionSize(control.batch_pages); + region = palloc0(region_size); + memcpy(region, &hdr, sizeof(hdr)); + memcpy(region + sizeof(hdr), &meta, sizeof(meta)); + + DWBBatchFilePath(path, batch_idx); + fd = OpenTransientFile(path, O_RDWR | PG_BINARY); + if (fd < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", path))); + errno = 0; + if (pg_pwrite(fd, region, region_size, 0) != (ssize_t) region_size || + pg_pwrite(fd, image.data, BLCKSZ, (off_t) region_size) != BLCKSZ) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not write file \"%s\": %m", path))); + if (CloseTransientFile(fd) != 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", path))); + pfree(region); + + PG_RETURN_VOID(); +} + +/* + * Rewrite pg_dwb/control with the given min_version (and a matching CRC): + * the state a ring left behind by a newer server would present after a + * binary downgrade. The next start must refuse it with the format-version + * FATAL, never with the "corrupt, remove pg_dwb" advice — the ring is + * intact and may hold unapplied repairs only the newer server can read. + */ +PG_FUNCTION_INFO_V1(test_dwb_set_control_min_version); +Datum +test_dwb_set_control_min_version(PG_FUNCTION_ARGS) +{ + DWBControlFileData control; + + check_dwb_enabled(); + + if (!DWBReadControlFile(&control, false, NULL)) + pg_unreachable(); + control.min_version = (uint32) PG_GETARG_INT32(0); + control.crc = DWBControlCrc(&control); + DWBWriteControlFile(&control); + + PG_RETURN_VOID(); +} From 29c55cd9f5cf040c96b72387d86dad1dbcdcbe67 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sun, 26 Jul 2026 09:24:55 +0300 Subject: [PATCH 19/52] Document the double write buffer (Stage 5) config.sgml gains io_torn_pages_protection and the dwb_* parameters, ordered as in postgresql.conf.sample, plus a note that full_page_writes is consulted only in full_pages mode. wal.sgml describes the double write buffer next to the discussion of partial page writes. backup.sgml notes that an active base backup forces page images back into WAL under double_writes; pg_basebackup's standby-backup limitations name the full_pages requirement on the primary; pg_rewind documents the full_pages requirement for a live source and the fate of pg_dwb during a rewind. monitoring.sgml adds the dwb object of pg_stat_io. --- doc/src/sgml/backup.sgml | 6 +- doc/src/sgml/config.sgml | 280 ++++++++++++++++++++++++++++ doc/src/sgml/monitoring.sgml | 7 + doc/src/sgml/ref/pg_basebackup.sgml | 6 +- doc/src/sgml/ref/pg_rewind.sgml | 13 ++ doc/src/sgml/wal.sgml | 24 +++ 6 files changed, 334 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml index 850971a435a36..2c61260dd01ca 100644 --- a/doc/src/sgml/backup.sgml +++ b/doc/src/sgml/backup.sgml @@ -819,7 +819,11 @@ test ! -f /mnt/server/archivedir/00000001000000A900000065 && cp pg_wal/0 to make a base backup. However, if you normally run the server with full_page_writes disabled, you might notice a drop in performance while the backup runs since full_page_writes is - effectively forced on during backup mode. + effectively forced on during backup mode. The same applies when the + server runs with set to + double_writes: page images are forced back into WAL + for the duration of the backup, because a base backup can legitimately + copy a torn page that only WAL replay with page images can repair. diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 607dafcb2ed16..ed9fe8307c92e 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3402,6 +3402,13 @@ include_dir 'conf.d' (see ). + + This parameter has an effect only when is set to + full_pages; the other modes determine the + protection against partial page writes themselves and ignore it. + + This parameter can only be set in the postgresql.conf file or on the server command line. @@ -3410,6 +3417,279 @@ include_dir 'conf.d' + + io_torn_pages_protection (enum) + + io_torn_pages_protection configuration parameter + + + + + Selects the mechanism that protects data files against torn + (partially written) pages after an operating system crash. + Valid values are full_pages, + double_writes and off. + The default is full_pages. + This parameter can only be set at server start. + + + + full_pages is the traditional protection: a full + image of each page is written to WAL on its first modification after + a checkpoint, under the control of the + parameter. + + + + double_writes replaces the page images in WAL + with a double write buffer: every data page + leaving shared buffers is first written and flushed to a small + reusable ring of files in the pg_dwb directory, + and only then written to its actual location. After a crash, pages + torn by interrupted writes are restored from their ring copies + before WAL replay begins. WAL carries no full page images at all in + this mode, which can substantially reduce WAL volume and the + associated commit latency spikes after checkpoints, at the price of + writing every flushed data page twice. See + for discussion. This mode + requires data checksums (see ); + the server refuses to start without them. An active base backup + temporarily forces page images back into WAL, because a backup can + legitimately copy a torn page that only WAL replay can repair. + + + + off disables torn page protection entirely + (full_page_writes is ignored). Nothing can + repair pages torn by a crash in this mode; use it only on storage + that guarantees atomic 8kB writes, under the same considerations as + turning off full_page_writes. + + + + The mode in effect is recorded in pg_control + and in the WAL stream. A standby tracks the mode of its primary: + replaying WAL that was generated without page images is refused if + the standby itself expects full_pages protection, + because a crash of that standby could tear pages its own recovery + could not repair. A standby running double_writes + repairs its own torn pages from its own ring and can follow a + primary in any mode. + + + + + + dwb_num_batches (integer) + + dwb_num_batches configuration parameter + + + + + Number of batches in the double write buffer ring. Together with + this sets the ring capacity: + the on-disk size of pg_dwb is roughly + dwb_num_batches × + dwb_batch_pages × 8kB. A larger ring + absorbs longer bursts of page writes before writers have to wait + for batches to be retired. The default is 64. + This parameter can only be set at server start. + + + + + + dwb_batch_pages (integer) + + dwb_batch_pages configuration parameter + + + + + Number of pages in one double write buffer batch. A batch is + written to the ring and flushed as a single unit, so this is the + unit of grouping for the ring's writes and fsyncs. The default + is 64. This parameter can only be set at server start. + + + + + + dwb_max_segments (integer) + + dwb_max_segments configuration parameter + + + + + Capacity of the shared table that tracks which data file segments + still need an fsync before their ring batches + can be reused. If it overflows, batch publication falls back to + retiring the batch synchronously, which is safe but slower. The + default is 4096. This parameter can only be set at server start. + + + + + + dwb_retire_workers (integer) + + dwb_retire_workers configuration parameter + + + + + Number of background workers that retire double write buffer + batches by flushing the covered data files, freeing ring space for + new writes. The workers consume + slots. Setting it to 0 + disables the pool and makes writers retire batches synchronously; + this is meant for testing only. The default is 1. + This parameter can only be set at server start. + + + + + + dwb_batch_timeout_ms (integer) + + dwb_batch_timeout_ms configuration parameter + + + + + Maximum time an open batch may collect pages before it is sealed + and written even if not full, bounding the latency a lone page + flush can spend waiting for company. + If this value is specified without units, it is taken as milliseconds. + The default is 10 milliseconds. + This parameter can only be set in the postgresql.conf + file or on the server command line. + + + + + + dwb_retire_interval_ms (integer) + + dwb_retire_interval_ms configuration parameter + + + + + Cycle time of each retire worker: how long a worker sleeps when it + finds no batches waiting for retirement. + If this value is specified without units, it is taken as milliseconds. + The default is 50 milliseconds. + This parameter can only be set in the postgresql.conf + file or on the server command line. + + + + + + dwb_writeback (boolean) + + dwb_writeback configuration parameter + + + + + When on, the server asks the kernel to start writing data pages + back to disk right after their double write, so that the eventual + retirement fsync finds the data already on its + way and acts as a cheap barrier rather than a full flush. Has no + effect on platforms without such a request. The default is + on. + This parameter can only be set in the postgresql.conf + file or on the server command line. + + + + + + dwb_slow_warn_ms (integer) + + dwb_slow_warn_ms configuration parameter + + + + + When a writer has waited this long for free ring space, a warning + is logged and non-critical background writing is paused so that the + remaining ring capacity serves user-facing work first. + If this value is specified without units, it is taken as milliseconds. + The default is 5 seconds. + This parameter can only be set in the postgresql.conf + file or on the server command line. + + + + + + dwb_slot_stuck_timeout_ms (integer) + + dwb_slot_stuck_timeout_ms configuration parameter + + + + + Time the writer of a sealed batch waits for the batch's remaining + pages to be staged before treating the batch as wedged and raising + a PANIC. This is a defensive limit that should + never be reached. + If this value is specified without units, it is taken as milliseconds. + The default is 30 seconds. + This parameter can only be set in the postgresql.conf + file or on the server command line. + + + + + + dwb_write_timeout_ms (integer) + + dwb_write_timeout_ms configuration parameter + + + + + When a writer has waited this long for free ring space, the action + selected by is taken. A wait + this long means batch retirement cannot keep up with the flow of + page writes (or the underlying storage has stalled). + If this value is specified without units, it is taken as milliseconds. + The default is 60 seconds. + This parameter can only be set in the postgresql.conf + file or on the server command line. + + + + + + dwb_on_stall (enum) + + dwb_on_stall configuration parameter + + + + + Action taken when a double write buffer wait exceeds + : warn + logs and keeps waiting, error aborts the waiting + transaction, and panic restarts the server. + Processes whose page writes cannot be abandoned, such as the + checkpointer, always escalate to panic. The + default is panic: a stall this long indicates a + storage-level problem, and a restart with crash recovery is + preferable to an indefinite hang. + This parameter can only be set in the postgresql.conf + file or on the server command line. + + + + wal_log_hints (boolean) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 5ac0e54688cb4..e960e0db49b5d 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -2662,6 +2662,13 @@ description | Waiting for a newly initialized WAL file to reach durable storage temp relation: Temporary relations. + + + dwb: The double write buffer ring in + pg_dwb (see + ). + + wal: Write Ahead Logs. diff --git a/doc/src/sgml/ref/pg_basebackup.sgml b/doc/src/sgml/ref/pg_basebackup.sgml index 9659f76042c5b..0c77bc2c1a9c6 100644 --- a/doc/src/sgml/ref/pg_basebackup.sgml +++ b/doc/src/sgml/ref/pg_basebackup.sgml @@ -115,7 +115,11 @@ PostgreSQL documentation All WAL records required for the backup must contain sufficient full-page writes, - which requires you to enable full_page_writes on the primary. + which requires you to enable full_page_writes on the + primary and to run it with + set to full_pages. In the other modes the primary + writes no page images and a standby has no way to request them, so a + backup taken from the standby is refused. diff --git a/doc/src/sgml/ref/pg_rewind.sgml b/doc/src/sgml/ref/pg_rewind.sgml index c696cec1a1c43..040ae5e2bd3ab 100644 --- a/doc/src/sgml/ref/pg_rewind.sgml +++ b/doc/src/sgml/ref/pg_rewind.sgml @@ -359,6 +359,19 @@ GRANT EXECUTE ON function pg_catalog.pg_read_binary_file(text, bigint, bigint, b + + When the source is a running server, it must run with + set to + full_pages and full_page_writes + enabled: pg_rewind reads data blocks from the + source while they may be concurrently written, and such a torn read is + only repaired by the full page images the target replays afterwards. A + cleanly shut down source can be used whatever its mode. The double + write buffer ring in pg_dwb is never copied from + the source, and the target's own ring contents are discarded when the + rewound cluster first starts. + + How It Works diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml index f3b86b26be905..215a43bd34352 100644 --- a/doc/src/sgml/wal.sgml +++ b/doc/src/sgml/wal.sgml @@ -179,6 +179,30 @@ (BBU) disk controllers do not prevent partial page writes unless they guarantee that data is written to the BBU as full (8kB) pages. + + An alternative protection against partial page writes is the + double write buffer, selected by setting + to + double_writes. Instead of recording page images in + WAL, every data page leaving shared buffers is first written and flushed + to a small reusable ring of files in the pg_dwb + directory, and only then written to its actual location; a batch of + pages is flushed to the ring with a single fsync, + and ring space is reused as soon as the covered data-file writes have + been made durable. After a crash, the server scans the ring before WAL + replay begins and rewrites every data page that is torn or older than + its ring copy, so replay always starts from intact pages. Since the + torn-page repair no longer depends on WAL contents, WAL carries no full + page images at all: WAL volume shrinks by their share, and the write + bursts that follow each checkpoint flatten out. In exchange every + flushed data page is written twice, which moves the cost from the WAL + device to the data-file write path. This mode relies on data checksums + to detect torn pages, so checksums must be enabled. The ring only + repairs pages torn by the local instance's own writes: it is excluded + from base backups, and a backup taken while this mode is active + temporarily carries forced page images in WAL instead (see + ). + PostgreSQL also protects against some kinds of data corruption on storage devices that may occur because of hardware errors or media failure over time, From 9e264790cde1b7de51ab15b02cc8cd8640cb06e6 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sun, 26 Jul 2026 10:43:51 +0300 Subject: [PATCH 20/52] Fix documentation overclaims and a test gap from the Stage 5 review The double_writes documentation promised that WAL carries no full page images at all; operations registering a block with REGBUF_FORCE_IMAGE still log one regardless of doPageWrites, so speak of the automatic first-modification-after-checkpoint images instead, in config.sgml, wal.sgml and the pg_basebackup standby-backup limitation. The ring also covers only permanent relations, so say "every permanent data page". The pg_stat_io intro in monitoring.sgml now mentions the double write buffer next to relations and WAL. 013_backup_start_point.pl asserted the absence of an apply-pass only after the second failed start; add the same negative check against the first start's log window. --- doc/src/sgml/config.sgml | 21 +++++++++++-------- doc/src/sgml/monitoring.sgml | 5 +++-- doc/src/sgml/ref/pg_basebackup.sgml | 5 +++-- doc/src/sgml/wal.sgml | 11 ++++++---- .../test_dwb/t/013_backup_start_point.pl | 5 ++++- 5 files changed, 29 insertions(+), 18 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index ed9fe8307c92e..ec446895bacc0 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3442,15 +3442,18 @@ include_dir 'conf.d' double_writes replaces the page images in WAL - with a double write buffer: every data page - leaving shared buffers is first written and flushed to a small - reusable ring of files in the pg_dwb directory, - and only then written to its actual location. After a crash, pages - torn by interrupted writes are restored from their ring copies - before WAL replay begins. WAL carries no full page images at all in - this mode, which can substantially reduce WAL volume and the - associated commit latency spikes after checkpoints, at the price of - writing every flushed data page twice. See + with a double write buffer: every permanent + data page leaving shared buffers is first written and flushed to a + small reusable ring of files in the pg_dwb + directory, and only then written to its actual location. After a + crash, pages torn by interrupted writes are restored from their + ring copies before WAL replay begins. The automatic page images + that full_pages mode writes into WAL on the + first modification after a checkpoint are not written in this mode + (operations that explicitly request a page image still log one), + which can substantially reduce WAL volume and the associated commit + latency spikes after checkpoints, at the price of writing every + flushed data page twice. See for discussion. This mode requires data checksums (see ); the server refuses to start without them. An active base backup diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index e960e0db49b5d..c041349f4ca16 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -2606,8 +2606,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage - Currently, I/O on relations (e.g. tables, indexes) and WAL activity are - tracked. However, relation I/O which bypasses shared buffers + Currently, I/O on relations (e.g. tables, indexes), WAL activity and the + double write buffer (see ) + are tracked. However, relation I/O which bypasses shared buffers (e.g. when moving a table from one tablespace to another) is currently not tracked. diff --git a/doc/src/sgml/ref/pg_basebackup.sgml b/doc/src/sgml/ref/pg_basebackup.sgml index 0c77bc2c1a9c6..aae8f4d4c8a77 100644 --- a/doc/src/sgml/ref/pg_basebackup.sgml +++ b/doc/src/sgml/ref/pg_basebackup.sgml @@ -118,8 +118,9 @@ PostgreSQL documentation which requires you to enable full_page_writes on the primary and to run it with set to full_pages. In the other modes the primary - writes no page images and a standby has no way to request them, so a - backup taken from the standby is refused. + does not write the automatic page images the backup depends on, and a + standby has no way to request them, so a backup taken from the + standby is refused. diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml index 215a43bd34352..92c763f08185a 100644 --- a/doc/src/sgml/wal.sgml +++ b/doc/src/sgml/wal.sgml @@ -184,16 +184,19 @@ double write buffer, selected by setting to double_writes. Instead of recording page images in - WAL, every data page leaving shared buffers is first written and flushed - to a small reusable ring of files in the pg_dwb + WAL, every permanent data page leaving shared buffers is first written + and flushed to a small reusable ring of files in the + pg_dwb directory, and only then written to its actual location; a batch of pages is flushed to the ring with a single fsync, and ring space is reused as soon as the covered data-file writes have been made durable. After a crash, the server scans the ring before WAL replay begins and rewrites every data page that is torn or older than its ring copy, so replay always starts from intact pages. Since the - torn-page repair no longer depends on WAL contents, WAL carries no full - page images at all: WAL volume shrinks by their share, and the write + torn-page repair no longer depends on WAL contents, the automatic page + images written on the first modification after a checkpoint disappear + from WAL (operations that explicitly request a page image still log + one): WAL volume shrinks by their share, and the write bursts that follow each checkpoint flatten out. In exchange every flushed data page is written twice, which moves the cost from the WAL device to the data-file write path. This mode relies on data checksums diff --git a/src/test/modules/test_dwb/t/013_backup_start_point.pl b/src/test/modules/test_dwb/t/013_backup_start_point.pl index cc797f2127574..2f3a6946c6ec7 100644 --- a/src/test/modules/test_dwb/t/013_backup_start_point.pl +++ b/src/test/modules/test_dwb/t/013_backup_start_point.pl @@ -74,6 +74,9 @@ qr/discarding double write buffer ring contents restored from a base backup/, $log_offset), 'the label start discarded the copied ring'); +ok( !$restored->log_contains( + qr/double write buffer recovery:/, $log_offset), + '... and ran no apply-pass'); ok(!-f "$pgdata/backup_label", 'the failed recovery consumed backup_label'); ok(-f "$pgdata/backup_label.old", '... renaming it out of the way'); @@ -95,6 +98,6 @@ 'backupStartPoint alone still discards the ring'); ok( !$restored->log_contains( qr/double write buffer recovery:/, $log_offset), - '... and no apply-pass ran on either start'); + '... and no apply-pass ran on the second start either'); done_testing(); From a95fa350478e1f5c454786de3adec05dfbc5acbe Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Wed, 29 Jul 2026 18:25:31 +0300 Subject: [PATCH 21/52] Apply pgindent, perltidy and perlcritic to the branch pgindent rewraps two comments in bgwriter.c and postmaster.c that fell short of the 78-column margin. perltidy reformats the 14 test_dwb TAP tests -- argument wrapping, paren tightness and continuation indentation. Run with version 20230309, the one pinned by src/tools/pgindent/README. perlcritic flagged the sysseek() calls in 001_dwb.pl as ignoring their return value. The defined-or guard does check it, but RequireCheckedSyscalls only recognizes "or die", which is the form used throughout the rest of the tree, so switch to it. Formatting only; the test_dwb suite (261 tests) still passes. --- src/backend/postmaster/bgwriter.c | 6 +- src/backend/postmaster/postmaster.c | 4 +- src/test/modules/test_dwb/t/001_dwb.pl | 87 +++++++++---------- .../modules/test_dwb/t/002_flushbuffer.pl | 47 +++++----- .../modules/test_dwb/t/003_backpressure.pl | 17 ++-- .../modules/test_dwb/t/004_retire_paths.pl | 46 +++++----- src/test/modules/test_dwb/t/005_standby.pl | 74 +++++++++------- src/test/modules/test_dwb/t/006_backup.pl | 49 +++++++---- src/test/modules/test_dwb/t/007_rewind.pl | 24 ++--- src/test/modules/test_dwb/t/008_modes.pl | 57 +++++++----- .../modules/test_dwb/t/009_fpw_transition.pl | 11 +-- src/test/modules/test_dwb/t/010_recovery.pl | 53 ++++++----- .../test_dwb/t/011_geometry_recovery.pl | 8 +- .../modules/test_dwb/t/012_apply_crafted.pl | 45 +++++----- .../test_dwb/t/013_backup_start_point.pl | 13 +-- src/test/modules/test_dwb/t/014_pg_upgrade.pl | 9 +- 16 files changed, 302 insertions(+), 248 deletions(-) diff --git a/src/backend/postmaster/bgwriter.c b/src/backend/postmaster/bgwriter.c index 81e9cdaf2b6c2..8efa39ea4054a 100644 --- a/src/backend/postmaster/bgwriter.c +++ b/src/backend/postmaster/bgwriter.c @@ -232,9 +232,9 @@ BackgroundWriterMain(const void *startup_data, size_t startup_data_len) ProcessMainLoopInterrupts(); /* - * Do one cycle of dirty-buffer writing. While a double write - * buffer stall has us paused (Stage A backpressure), sit the round - * out instead of queueing more flushes behind an exhausted ring; + * Do one cycle of dirty-buffer writing. While a double write buffer + * stall has us paused (Stage A backpressure), sit the round out + * instead of queueing more flushes behind an exhausted ring; * user-facing paths keep their reserve, we retry after the delay. */ if (DWBIsEnabled() && DWBWritesPaused()) diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index 170cdc67dfc27..2ec284280a106 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -929,8 +929,8 @@ PostmasterMain(int argc, char *argv[]) ApplyLauncherRegister(); /* - * Register the double write buffer retire workers, for the same - * reason: the ring cannot circulate without them. + * Register the double write buffer retire workers, for the same reason: + * the ring cannot circulate without them. */ DWBRetireWorkersRegister(); diff --git a/src/test/modules/test_dwb/t/001_dwb.pl b/src/test/modules/test_dwb/t/001_dwb.pl index a43b5e51f101c..ba1c43547292a 100644 --- a/src/test/modules/test_dwb/t/001_dwb.pl +++ b/src/test/modules/test_dwb/t/001_dwb.pl @@ -30,11 +30,11 @@ # --- single-session cycles --------------------------------------------- -is( $node->safe_psql('postgres', 'SELECT test_dwb_cycle(40)'), +is($node->safe_psql('postgres', 'SELECT test_dwb_cycle(40)'), '3', 'overflow-sealed cycle retires three batches'); # eager retirement reuses batch file 0 within the cycle: its final content # is the 8-slot tail write, plus 16 slots in batch file 1 -is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), +is($node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), '24', 'all surviving slots validate (meta_crc, generation, image_crc)'); # --- torn copies are rejected by the on-disk CRCs ---------------------- @@ -48,9 +48,9 @@ sub flip_byte { my ($file, $offset) = @_; open my $bf, '+<:raw', $file or die "open $file: $!"; - sysseek($bf, $offset, 0) // die "seek: $!"; + sysseek($bf, $offset, 0) or die "seek: $!"; die "read: $!" unless sysread($bf, my $byte, 1) == 1; - sysseek($bf, $offset, 0) // die "seek: $!"; + sysseek($bf, $offset, 0) or die "seek: $!"; die "write: $!" unless syswrite($bf, chr(ord($byte) ^ 0xFF), 1) == 1; close $bf; return; @@ -58,12 +58,12 @@ sub flip_byte # a torn image: one flipped byte inside slot 0's page image flip_byte($bfile, 4096 + 100); -is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), +is($node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), '23', 'image_crc rejects a torn page image'); # a torn meta: one flipped byte inside slot 1's meta (offset 24 + 56) flip_byte($bfile, 24 + 56); -is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), +is($node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), '22', 'meta_crc rejects a torn slot meta'); # --- concurrent writers over a small ring ------------------------------ @@ -74,15 +74,18 @@ sub flip_byte print $fh "SELECT test_dwb_stress(1, 40);\n"; close $fh; $node->command_ok( - [ 'pgbench', '-n', '-c', '3', '-j', '3', '-t', '30', '-f', $script, - 'postgres' ], + [ + 'pgbench', '-n', '-c', '3', '-j', '3', + '-t', '30', '-f', $script, 'postgres' + ], 'concurrent stress over a small ring'); like( $node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=16 allocated=0 sealed=0 written=0 fsynced=0 data_written=0 retiring=0/, 'ring fully retired after concurrent stress'); my $valid = $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'); -cmp_ok($valid, '>', 0, 'ring holds valid current-generation slots after stress'); +cmp_ok($valid, '>', 0, + 'ring holds valid current-generation slots after stress'); cmp_ok($valid, '<=', 16 * 16, 'slot count bounded by the ring capacity'); # --- restart bumps the durable generation ------------------------------ @@ -94,10 +97,9 @@ sub flip_byte my $stale = $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(false)'); cmp_ok($stale, '>', 0, 'ring holds slots before the restart check'); $node->restart; -is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), +is($node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), '0', 'no slot belongs to the new generation after restart'); -cmp_ok( - $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(false)'), +cmp_ok($node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(false)'), '>', 0, 'stale slots still CRC-valid, only the generation gates them'); # --- process exit cleanup ---------------------------------------------- @@ -108,17 +110,16 @@ sub flip_byte my $bg = $node->background_psql('postgres'); $bg->query_safe('SELECT test_dwb_leak(3, false)'); $bg->quit; -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/allocated=1/, 'abandoned batch stays open'); -is( $node->safe_psql('postgres', 'SELECT test_dwb_force_seal()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_force_seal()'), 't', 'abandoned batch seals'); $node->poll_query_until('postgres', "SELECT test_dwb_states() LIKE '%retiring=1%'") or die 'timed out waiting for the abandoned batch to reach RETIRING'; -is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_retire()'), '1', 'abandoned batch retires'); -is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), +is($node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), '0', 'aborted slots are not apply candidates'); # A backend dies after publishing: the copies are still written out and @@ -127,19 +128,18 @@ sub flip_byte $bg = $node->background_psql('postgres'); $bg->query_safe('SELECT test_dwb_leak(3, true)'); $bg->quit; -is( $node->safe_psql('postgres', 'SELECT test_dwb_force_seal()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_force_seal()'), 't', 'orphaned batch seals'); # the dead backend's ProcExit may still be releasing its refs: wait for # the FSYNCED -> RETIRING hand-off instead of assuming it already happened $node->poll_query_until('postgres', "SELECT test_dwb_states() LIKE '%retiring=1%'") or die 'timed out waiting for the orphaned batch to reach RETIRING'; -is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_retire()'), '1', 'orphaned batch retires'); -is( $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), +is($node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), '3', 'published slots of a dead backend survive and validate'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=16/, 'ring fully idle after the orphan hand-off'); # A backend dies holding the LAST ref of an already-durable batch: the exit @@ -152,7 +152,7 @@ sub flip_byte $node->poll_query_until('postgres', "SELECT test_dwb_states() LIKE '%retiring=1%'") or die 'timed out waiting for the exit-time publication'; -is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_retire()'), '1', 'batch published from the exit backstop retires'); # --- transaction abort releases refs (ResourceOwner path) --------------- @@ -163,12 +163,11 @@ sub flip_byte my ($rc, $out, $err) = $node->psql('postgres', 'SELECT test_dwb_abort_release(3, false)'); isnt($rc, 0, 'deliberate abort with pending refs reported'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/allocated=1/, 'batch of the aborted transaction stays open'); -is( $node->safe_psql('postgres', 'SELECT test_dwb_force_seal()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_force_seal()'), 't', 'batch of the aborted transaction seals'); -is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_retire()'), '1', 'batch of the aborted transaction retires'); # An ERROR after the batch is durable: the abort cleanup goes through the @@ -177,10 +176,9 @@ sub flip_byte ($rc, $out, $err) = $node->psql('postgres', 'SELECT test_dwb_abort_after_fsync()'); isnt($rc, 0, 'deliberate abort after batch fsync reported'); -is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_retire()'), '1', 'batch of the post-fsync abort retires'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=16/, 'ring idle after the abort scenarios'); # --- torn data page repaired from the batch copy on abort ---------------- @@ -191,7 +189,8 @@ sub flip_byte # durable batch copy (DWBRewriteAbandonedSlot). The restart proves the # repair reached the data file: the buffer cache is dropped, and with data # checksums a block left torn would make the read below fail. -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE dwb_repair AS SELECT g AS id, repeat('r', 64) AS pad FROM generate_series(1, 100) g; )); @@ -202,10 +201,10 @@ sub flip_byte $node->psql('postgres', "SELECT test_dwb_torn_repair($filenode, 0)"); isnt($rc, 0, 'deliberate abort after tearing the data page reported'); like($err, qr/deliberate abort after tearing/, 'the tear scenario ran'); -is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_retire()'), '1', 'batch of the torn-page scenario retires'); $node->restart; -is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_repair'), +is($node->safe_psql('postgres', 'SELECT count(*) FROM dwb_repair'), '100', 'torn block repaired from the batch copy (checksum-clean read)'); # --- background writers leave the eviction reserve ----------------------- @@ -216,17 +215,15 @@ sub flip_byte $bg = $node->background_psql('postgres'); my $bg_taken = $bg->query_safe('SELECT test_dwb_fill_ring(true)'); cmp_ok($bg_taken, '>', 0, 'background class filled the ring'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=2 /, 'background class stops at DWB_EVICT_RESERVE free batches'); my $ev_taken = $bg->query_safe('SELECT test_dwb_fill_ring(false)'); cmp_ok($ev_taken, '>', 0, 'eviction class still opens batches'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=0 /, 'eviction class may take the ring to zero'); $bg->quit; $node->poll_query_until('postgres', - "SELECT CASE WHEN test_dwb_force_seal(false) IS NOT NULL THEN " + "SELECT CASE WHEN test_dwb_force_seal(false) IS NOT NULL THEN " . "CASE WHEN test_dwb_force_seal(true) IS NOT NULL THEN " . "CASE WHEN test_dwb_retire() >= 0 THEN " . "test_dwb_states() LIKE 'free=16 %' END END END") @@ -234,12 +231,10 @@ sub flip_byte # --- stale open must not hijack a reopened index ------------------------ -($rc, $out, $err) = - $node->psql('postgres', 'SELECT test_dwb_open_stale()'); +($rc, $out, $err) = $node->psql('postgres', 'SELECT test_dwb_open_stale()'); is($rc, 0, 'stale open leaves the live reopened batch alone') or diag($err); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/free=16/, 'ring idle after the stale-open scenario'); # --- a geometry change recreates the ring ------------------------------ @@ -257,12 +252,12 @@ sub flip_byte $log_offset), 'geometry change recreates the ring'); ok( $node->log_contains( - qr/ring opened: 32 batches of 16 pages, generation 1\b/, - $log_offset), + qr/ring opened: 32 batches of 16 pages, generation 1\b/, $log_offset), 'recreated ring opens with a fresh generation'); $node->stop; $log_offset = -s $node->logfile; -$node->append_conf('postgresql.conf', +$node->append_conf( + 'postgresql.conf', 'dwb_num_batches = 16 dwb_batch_pages = 32'); $node->start; @@ -308,7 +303,7 @@ sub flip_byte 'io_torn_pages_protection = double_writes'); $ret = $node2->start(fail_ok => 1); is($ret, 0, 'start refused without data checksums'); -ok( $node2->log_contains('requires data checksums'), +ok($node2->log_contains('requires data checksums'), 'checksum requirement reported'); done_testing(); diff --git a/src/test/modules/test_dwb/t/002_flushbuffer.pl b/src/test/modules/test_dwb/t/002_flushbuffer.pl index da5207c452006..fadb76e7c616a 100644 --- a/src/test/modules/test_dwb/t/002_flushbuffer.pl +++ b/src/test/modules/test_dwb/t/002_flushbuffer.pl @@ -32,13 +32,14 @@ # the workers start asynchronously once the server is up $node->poll_query_until('postgres', - "SELECT count(*) = 2 FROM pg_stat_activity WHERE backend_type = 'dwb retire worker'") - or die 'timed out waiting for the retire workers to start'; + "SELECT count(*) = 2 FROM pg_stat_activity WHERE backend_type = 'dwb retire worker'" +) or die 'timed out waiting for the retire workers to start'; pass('both retire workers are running'); # --- a real workload flows through the ring ------------------------------ -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE dwb_t AS SELECT g AS id, repeat('x', 300) AS filler FROM generate_series(1, 50000) g; @@ -46,13 +47,12 @@ )); $node->safe_psql('postgres', 'CHECKPOINT'); -is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), +is($node->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), '50000', 'workload survived the DWB write path'); # The workload far exceeds shared_buffers, so evictions must have staged # real pages into the ring under the current generation. -cmp_ok( - $node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), +cmp_ok($node->safe_psql('postgres', 'SELECT test_dwb_ring_slots(true)'), '>', 0, 'real pages were staged into the ring'); # --- the worker pool retires everything ---------------------------------- @@ -66,8 +66,10 @@ is( $node->safe_psql( 'postgres', - "SELECT sum(writes) > 0 AND sum(fsyncs) > 0 FROM pg_stat_io WHERE object = 'dwb'"), - 't', 'pg_stat_io shows double write buffer writes and fsyncs'); + "SELECT sum(writes) > 0 AND sum(fsyncs) > 0 FROM pg_stat_io WHERE object = 'dwb'" + ), + 't', + 'pg_stat_io shows double write buffer writes and fsyncs'); # --- crash recovery: data intact, generation bumped ---------------------- @@ -79,7 +81,7 @@ $node->stop('immediate'); $node->start; -is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), +is($node->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), '50000', 'data intact after crash recovery'); # --- unlogged relations bypass the double write buffer ------------------- @@ -90,7 +92,8 @@ # buffers. The assertion reads the ring itself: a broken gate would leave # dwb_ul's tags in batch files, and no stray permanent-page flush can fake # that. -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE UNLOGGED TABLE dwb_ul AS SELECT g AS id, repeat('u', 300) AS filler FROM generate_series(1, 1000) g; @@ -101,21 +104,25 @@ my $t_filenode = $node->safe_psql('postgres', "SELECT pg_relation_filenode('dwb_t')"); my $rel_pre = $node->safe_psql('postgres', - "SELECT COALESCE(sum(writes), 0) FROM pg_stat_io " + "SELECT COALESCE(sum(writes), 0) FROM pg_stat_io " . "WHERE object = 'relation' AND backend_type = 'checkpointer'"); $node->restart; cmp_ok( - $node->safe_psql('postgres', + $node->safe_psql( + 'postgres', "SELECT COALESCE(sum(writes), 0) FROM pg_stat_io " . "WHERE object = 'relation' AND backend_type = 'checkpointer'"), - '>', $rel_pre, 'shutdown checkpoint flushed the unlogged pages'); -is( $node->safe_psql('postgres', - "SELECT test_dwb_ring_rel_slots($ul_filenode)"), - '0', 'no unlogged page ever entered the ring'); + '>', $rel_pre, + 'shutdown checkpoint flushed the unlogged pages'); +is( $node->safe_psql( + 'postgres', "SELECT test_dwb_ring_rel_slots($ul_filenode)"), + '0', + 'no unlogged page ever entered the ring'); cmp_ok( - $node->safe_psql('postgres', - "SELECT test_dwb_ring_rel_slots($t_filenode)"), - '>', 0, 'permanent pages did enter the ring (control)'); + $node->safe_psql( + 'postgres', "SELECT test_dwb_ring_rel_slots($t_filenode)"), + '>', 0, + 'permanent pages did enter the ring (control)'); # --- the write path is self-sufficient without the worker pool ----------- @@ -127,7 +134,7 @@ $node->safe_psql('postgres', "UPDATE dwb_t SET filler = repeat('n', 300) WHERE id % 5 = 0"); $node->safe_psql('postgres', 'CHECKPOINT'); -is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), +is($node->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), '50000', 'workload survived the no-pool write path'); $node->poll_query_until('postgres', "SELECT test_dwb_states() LIKE 'free=16 %'") diff --git a/src/test/modules/test_dwb/t/003_backpressure.pl b/src/test/modules/test_dwb/t/003_backpressure.pl index 46cc9bc9e0c57..a674254ebff10 100644 --- a/src/test/modules/test_dwb/t/003_backpressure.pl +++ b/src/test/modules/test_dwb/t/003_backpressure.pl @@ -46,7 +46,8 @@ # Dirty pages for the checkpointer scenario, created while the ring is # still healthy and small enough to stay in shared_buffers. -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE dwb_dirty AS SELECT g AS id, repeat('d', 300) AS filler FROM generate_series(1, 1000) g; @@ -66,7 +67,8 @@ # The victim outgrows shared_buffers, so it must evict its own dirty pages # through the exhausted ring. Its rollback drops its buffers unwritten, # leaving the pool clean for the sessions that follow. -my ($rc, $out, $err) = $node->psql('postgres', q( +my ($rc, $out, $err) = $node->psql( + 'postgres', q( CREATE TABLE dwb_victim AS SELECT g AS id, repeat('v', 300) AS filler FROM generate_series(1, 80000) g; @@ -77,7 +79,7 @@ qr/double write buffer retirement made no progress/, 'stall ERROR reported to the writer'); -is( $node->safe_psql('postgres', 'SELECT 1'), +is($node->safe_psql('postgres', 'SELECT 1'), '1', 'cluster alive after the writer ERROR'); $node->safe_psql('postgres', @@ -88,7 +90,7 @@ # CASEs order the side effects before the state probe). $filler->quit; $node->poll_query_until('postgres', - "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " + "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " . "CASE WHEN test_dwb_retire() >= 0 THEN " . "test_dwb_states() LIKE 'free=16 %' END END") or die 'timed out waiting for the ring to drain after the ERROR scenario'; @@ -120,7 +122,7 @@ 'double write buffer retirement made no progress', $log_offset), 'checkpointer stall escalated to the role-policy PANIC'); -is( $node->safe_psql('postgres', 'SELECT count(*) FROM dwb_dirty'), +is($node->safe_psql('postgres', 'SELECT count(*) FROM dwb_dirty'), '1000', 'data intact after crash recovery'); # --- Stage A warning fires on the real clock ------------------------------ @@ -153,9 +155,10 @@ $filler->quit; $node->poll_query_until('postgres', - "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " + "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " . "CASE WHEN test_dwb_retire() >= 0 THEN " . "test_dwb_states() LIKE 'free=16 %' END END") - or die 'timed out waiting for the ring to drain after the slow-warn scenario'; + or die + 'timed out waiting for the ring to drain after the slow-warn scenario'; done_testing(); diff --git a/src/test/modules/test_dwb/t/004_retire_paths.pl b/src/test/modules/test_dwb/t/004_retire_paths.pl index 0442090abba2e..8f20f512603b6 100644 --- a/src/test/modules/test_dwb/t/004_retire_paths.pl +++ b/src/test/modules/test_dwb/t/004_retire_paths.pl @@ -36,7 +36,8 @@ # --- a CHECKPOINT alone retires a batch (ProcessSyncRequests path) ------- -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE dwb_ckpt AS SELECT g AS id, repeat('c', 64) AS pad FROM generate_series(1, 100) g; )); @@ -52,17 +53,15 @@ $node->safe_psql('postgres', "SELECT test_dwb_checkpoint_pending($filenode)"); $node->safe_psql('postgres', 'SELECT test_dwb_states()'); $node->safe_psql('postgres', 'CHECKPOINT'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/^free=64 /, 'warmup batch retired'); $node->safe_psql('postgres', "SELECT test_dwb_checkpoint_pending($filenode)"); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), - qr/retiring=1$/, 'one batch parked in RETIRING with a pending sync request'); +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/retiring=1$/, + 'one batch parked in RETIRING with a pending sync request'); $node->safe_psql('postgres', 'CHECKPOINT'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/^free=64 /, 'CHECKPOINT alone retired the parked batch'); # --- a checkpoint tolerates a live ALLOCATED batch ------------------------ @@ -83,17 +82,18 @@ $holder->query_safe('SELECT test_dwb_leak(1, true)'); my $one_open = 'free=63 allocated=1 sealed=0 written=0 fsynced=0 data_written=0 retiring=0'; -is( $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_states()'), $one_open, 'an open ALLOCATED batch is live before the checkpoint'); $node->safe_psql('postgres', 'CHECKPOINT'); -is( $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_states()'), $one_open, 'CHECKPOINT completed and left the open batch alone'); $holder->quit; $node->poll_query_until('postgres', - "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " + "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " . "CASE WHEN test_dwb_retire() >= 0 THEN " . "test_dwb_states() LIKE 'free=64 %' END END") - or die 'timed out waiting for the open batch to drain after the holder quit'; + or die + 'timed out waiting for the open batch to drain after the holder quit'; pass('abandoned open batch drained'); # --- segment hash overflow degrades to synchronous retire ---------------- @@ -107,14 +107,14 @@ $err, qr/double write buffer segment hash is full/, 'hash overflow warning reached the publisher'); -ok( $node->log_contains('double write buffer segment hash is full', - $log_offset), +ok( $node->log_contains( + 'double write buffer segment hash is full', $log_offset), 'hash overflow logged'); # The batches parked in RETIRING drain through the normal sweep (dropped # fake segments count as covered); the OOM-retired ones are already free. $node->poll_query_until('postgres', - "SELECT CASE WHEN test_dwb_retire() >= 0 THEN " + "SELECT CASE WHEN test_dwb_retire() >= 0 THEN " . "test_dwb_states() LIKE 'free=64 %' END") or die 'timed out waiting for the ring to drain after the hash overflow'; pass('ring drained after the hash overflow'); @@ -127,15 +127,13 @@ # unrelated non-MD entry. The parked batch must still be RETIRING — a # consumed stale snapshot would have freed it without durability. $node->safe_psql('postgres', 'SELECT test_dwb_park(98000)'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/retiring=1$/, 'batch parked for the stale-snapshot scenario'); $node->safe_psql('postgres', 'SELECT test_dwb_stale_snapshot(98000)'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/retiring=1$/, 'stale snapshot dropped, parked batch still RETIRING'); $node->poll_query_until('postgres', - "SELECT CASE WHEN test_dwb_retire() >= 0 THEN " + "SELECT CASE WHEN test_dwb_retire() >= 0 THEN " . "test_dwb_states() LIKE 'free=64 %' END") or die 'timed out waiting for the stale-snapshot batch to drain'; @@ -161,15 +159,13 @@ $err, qr/could not fsync file/, 'soft fsync failure reported as a WARNING'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/retiring=1$/, 'batch stays RETIRING after the soft fsync failure'); rmdir $segdir or die "rmdir $segdir: $!"; -is( $node->safe_psql('postgres', 'SELECT test_dwb_retire()'), +is($node->safe_psql('postgres', 'SELECT test_dwb_retire()'), '1', 'released claim lets the next sweep cover the segment'); -like( - $node->safe_psql('postgres', 'SELECT test_dwb_states()'), +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), qr/^free=64 /, 'ring drained after the soft-failure scenario'); done_testing(); diff --git a/src/test/modules/test_dwb/t/005_standby.pl b/src/test/modules/test_dwb/t/005_standby.pl index 2eded44aec3e8..6391a4d5071fb 100644 --- a/src/test/modules/test_dwb/t/005_standby.pl +++ b/src/test/modules/test_dwb/t/005_standby.pl @@ -59,7 +59,8 @@ # --- replay traffic flows through the standby ring ----------------------- -$primary->safe_psql('postgres', q( +$primary->safe_psql( + 'postgres', q( CREATE TABLE dwb_t AS SELECT g AS id, repeat('x', 300) AS filler FROM generate_series(1, 10000) g; @@ -74,14 +75,16 @@ # XLOG_RUNNING_XACTS record the primary's CHECKPOINT above emitted, but # that is asynchronous to wait_for_catchup — hence the poll. $standby->poll_query_until('postgres', - "SELECT COALESCE(sum(writes), 0) > 0 FROM pg_stat_io " + "SELECT COALESCE(sum(writes), 0) > 0 FROM pg_stat_io " . "WHERE object = 'dwb' AND backend_type = 'startup'") or die 'timed out waiting for startup-process DWB writes on the standby'; pass('replay evictions flowed through the standby ring'); -is( $standby->safe_psql('postgres', +is( $standby->safe_psql( + 'postgres', "SELECT count(*) FROM dwb_t WHERE filler = repeat('y', 300)"), - '1000', 'replayed page contents are correct'); + '1000', + 'replayed page contents are correct'); # --- FlushBuffer on the standby advances minRecoveryPoint ---------------- @@ -97,9 +100,11 @@ "UPDATE dwb_t SET filler = repeat('m', 300) WHERE id % 9 = 0"); $primary->wait_for_catchup($standby); $standby->poll_query_until('postgres', - "SELECT min_recovery_end_lsn > '$mrp_before'::pg_lsn FROM pg_control_recovery()") + "SELECT min_recovery_end_lsn > '$mrp_before'::pg_lsn FROM pg_control_recovery()" + ) or die 'minRecoveryPoint did not advance from replay-driven flushes alone'; -pass('replay-driven flushes advanced minRecoveryPoint without a restartpoint'); +pass( + 'replay-driven flushes advanced minRecoveryPoint without a restartpoint'); # and the ring keeps circulating: the worker drains it back to all-free $standby->poll_query_until('postgres', @@ -121,15 +126,10 @@ '--port' => $standby->port, '--checkpoint' => 'fast' ]); -ok(!-f "$refused_path/PG_VERSION", - 'base backup from the standby is refused'); -like( - $err, - qr/WAL generated without full page images was replayed/, +ok(!-f "$refused_path/PG_VERSION", 'base backup from the standby is refused'); +like($err, qr/WAL generated without full page images was replayed/, '... loudly'); -like( - $err, - qr/io_torn_pages_protection/, +like($err, qr/io_torn_pages_protection/, '... with a hint naming the real knob'); # --- the standby survives its own crash ---------------------------------- @@ -145,7 +145,7 @@ $primary->safe_psql('postgres', "INSERT INTO dwb_t VALUES (100001, 'after standby crash')"); $primary->wait_for_catchup($standby); -is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), +is($standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), '10001', 'replication resumed after the standby crash'); # --- the primary survives its own crash ---------------------------------- @@ -155,7 +155,7 @@ $primary->safe_psql('postgres', "INSERT INTO dwb_t VALUES (100002, 'after primary crash')"); $primary->wait_for_catchup($standby); -is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), +is($standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), '10002', 'replication resumed after the primary crash'); # --- a torn page on the standby is repaired by its own apply-pass --------- @@ -164,13 +164,14 @@ # DB_IN_ARCHIVE_RECOVERY — the branch that may raise minRecoveryPoint — # and must repair from the standby's OWN ring: the replayed WAL carries no # page images that could do it instead. -$primary->safe_psql('postgres', q( +$primary->safe_psql( + 'postgres', q( CREATE TABLE ts_repair AS SELECT g AS id FROM generate_series(1, 100) g; )); $primary->safe_psql('postgres', 'CHECKPOINT'); $primary->wait_for_catchup($standby); -my $ts_path = $primary->safe_psql('postgres', - "SELECT pg_relation_filepath('ts_repair')"); +my $ts_path = + $primary->safe_psql('postgres', "SELECT pg_relation_filepath('ts_repair')"); my $ts_relnum = $primary->safe_psql('postgres', "SELECT relfilenode FROM pg_class WHERE relname = 'ts_repair'"); @@ -191,7 +192,7 @@ $standby_log_offset), 'the crashed standby repaired its torn page from its own ring'); $primary->wait_for_catchup($standby); -is( $standby->safe_psql('postgres', 'SELECT count(*) FROM ts_repair'), +is($standby->safe_psql('postgres', 'SELECT count(*) FROM ts_repair'), '100', 'the repaired standby page reads whole'); # --- promotion with a replay backlog ------------------------------------- @@ -205,34 +206,41 @@ $standby->poll_query_until('postgres', "SELECT pg_get_wal_replay_pause_state() = 'paused'") or die 'timed out waiting for replay to pause'; -$primary->safe_psql('postgres', q( +$primary->safe_psql( + 'postgres', q( UPDATE dwb_t SET filler = repeat('p', 300) WHERE id % 3 = 0; INSERT INTO dwb_t VALUES (100003, 'burst tail'); )); $primary->wait_for_catchup($standby, 'flush', $primary->lsn('write')); -is( $standby->safe_psql('postgres', +is( $standby->safe_psql( + 'postgres', 'SELECT pg_last_wal_replay_lsn() < pg_last_wal_receive_lsn()'), - 't', 'a real replay backlog exists at promotion time'); + 't', + 'a real replay backlog exists at promotion time'); $standby->promote; -is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), +is($standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), '10003', 'promoted standby replayed the whole backlog'); -is( $standby->safe_psql('postgres', +is( $standby->safe_psql( + 'postgres', "SELECT count(*) FROM dwb_t WHERE filler = repeat('p', 300)"), - '3334', 'backlog page contents are correct'); -is( $standby->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), + '3334', + 'backlog page contents are correct'); +is($standby->safe_psql('postgres', 'SELECT pg_is_in_recovery()'), 'f', 'standby left recovery'); # --- the promoted node is a full DWB primary ----------------------------- -my $tl2_start = $standby->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); -$standby->safe_psql('postgres', q( +my $tl2_start = + $standby->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); +$standby->safe_psql( + 'postgres', q( UPDATE dwb_t SET filler = repeat('q', 300) WHERE id % 5 = 0; INSERT INTO dwb_t VALUES (100004, 'after promotion'); )); my $tl2_end = $standby->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); $standby->safe_psql('postgres', 'CHECKPOINT'); -is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), +is($standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_t'), '10004', 'promoted node accepts writes'); $standby->poll_query_until('postgres', "SELECT test_dwb_states() LIKE 'free=16 %'") @@ -242,9 +250,11 @@ # the new timeline still carries no page images my ($waldump, $walerr) = run_command( [ - 'pg_waldump', '--path' => $standby->data_dir . '/pg_wal', + 'pg_waldump', + '--path' => $standby->data_dir . '/pg_wal', '--timeline' => 2, - '--start' => $tl2_start, '--end' => $tl2_end + '--start' => $tl2_start, + '--end' => $tl2_end ]); is($walerr, '', 'pg_waldump read the post-promotion window cleanly'); like($waldump, qr/Heap/, 'the window covers the post-promotion update'); diff --git a/src/test/modules/test_dwb/t/006_backup.pl b/src/test/modules/test_dwb/t/006_backup.pl index 6a174619acf98..b2fbff633252b 100644 --- a/src/test/modules/test_dwb/t/006_backup.pl +++ b/src/test/modules/test_dwb/t/006_backup.pl @@ -27,7 +27,8 @@ )); $node->start; -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE dwb_fpi AS SELECT g AS id, repeat('f', 64) AS pad FROM generate_series(1, 100) g; )); @@ -43,13 +44,16 @@ # stays meaningful. $node->safe_psql('postgres', 'CHECKPOINT'); my $lsn0 = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); -$node->safe_psql('postgres', "UPDATE dwb_fpi SET pad = repeat('a', 64) WHERE id = 1"); +$node->safe_psql('postgres', + "UPDATE dwb_fpi SET pad = repeat('a', 64) WHERE id = 1"); my $lsn1 = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); my ($waldump, $walerr) = run_command( [ - 'pg_waldump', '--path' => $node->data_dir . '/pg_wal', - '--start' => $lsn0, '--end' => $lsn1 + 'pg_waldump', + '--path' => $node->data_dir . '/pg_wal', + '--start' => $lsn0, + '--end' => $lsn1 ]); is($walerr, '', 'pg_waldump read the no-backup window cleanly'); like($waldump, qr/Heap/, 'the WAL window covers the update'); @@ -65,13 +69,16 @@ $bk->query_safe("SELECT pg_backup_start('dwb_fpi_probe', true)"); my $lsn2 = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); -$node->safe_psql('postgres', "UPDATE dwb_fpi SET pad = repeat('b', 64) WHERE id = 2"); +$node->safe_psql('postgres', + "UPDATE dwb_fpi SET pad = repeat('b', 64) WHERE id = 2"); my $lsn3 = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); ($waldump, $walerr) = run_command( [ - 'pg_waldump', '--path' => $node->data_dir . '/pg_wal', - '--start' => $lsn2, '--end' => $lsn3 + 'pg_waldump', + '--path' => $node->data_dir . '/pg_wal', + '--start' => $lsn2, + '--end' => $lsn3 ]); is($walerr, '', 'pg_waldump read the backup window cleanly'); like( @@ -85,7 +92,7 @@ # --- the backup keeps pg_dwb as an empty directory ------------------------ # guard against a vacuous emptiness assert: the source ring is non-empty -ok(-f $node->data_dir . '/pg_dwb/control', +ok( -f $node->data_dir . '/pg_dwb/control', 'the source cluster has ring files to exclude'); my $backup_path = $node->backup_dir . '/content_check'; @@ -98,7 +105,9 @@ '--checkpoint' => 'fast' ]); ok(-f "$backup_path/PG_VERSION", 'backup completed'); -unlike($err, qr/WARNING|skipping special file/, +unlike( + $err, + qr/WARNING|skipping special file/, 'pg_basebackup issued no warnings'); ok(-d "$backup_path/pg_dwb", 'backup contains a pg_dwb directory'); { @@ -122,7 +131,7 @@ qr/discarding double write buffer ring contents/, $restored_log_offset), '... without claiming to discard the empty restored pg_dwb'); -is( $restored->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpi'), +is($restored->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpi'), '100', 'restored data is intact'); $restored->stop; @@ -138,7 +147,7 @@ # pins that the wipe comes before any ring-state read. my $planted = PostgreSQL::Test::Cluster->new('dwb_planted'); $planted->init_from_backup($node, 'content_check'); -ok(-f $planted->data_dir . '/backup_label', +ok( -f $planted->data_dir . '/backup_label', 'the restore still carries backup_label'); append_to_file($planted->data_dir . '/pg_dwb/control', 'torn by the tool'); append_to_file($planted->data_dir . '/pg_dwb/batch_9999', 'foreign slots'); @@ -150,7 +159,8 @@ $planted_log_offset), 'the restored ring is discarded'); ok( !$planted->log_contains( - qr/double write buffer recovery:/, $planted_log_offset), + qr/double write buffer recovery:/, + $planted_log_offset), '... without an apply-pass over it'); ok( $planted->log_contains( qr/ring opened: 16 batches of 16 pages, generation 1\b/, @@ -158,7 +168,7 @@ '... and a fresh ring is created cold'); ok(!-f $planted->data_dir . '/pg_dwb/batch_9999', 'the foreign ring files are gone'); -is( $planted->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpi'), +is($planted->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpi'), '100', 'restored data is intact'); # once the backup recovery is over the guard is gone: an ordinary crash @@ -194,9 +204,9 @@ 'a full_pages restore discards the planted ring too'); ok(!-f $planted_fp->data_dir . '/pg_dwb/batch_9999', '... removing the foreign files'); -ok( !$planted_fp->log_contains(qr/ring opened/, $planted_fp_log_offset), +ok(!$planted_fp->log_contains(qr/ring opened/, $planted_fp_log_offset), '... without creating a ring it will not use'); -is( $planted_fp->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpi'), +is($planted_fp->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpi'), '100', 'restored data is intact under full_pages'); $planted_fp->stop; @@ -223,10 +233,13 @@ '--port' => $node->port, '--checkpoint' => 'fast' ]); - ok(-f "$link_backup/PG_VERSION", 'backup of the symlinked ring completed'); - unlike($err, qr/WARNING|skipping special file/, + ok(-f "$link_backup/PG_VERSION", + 'backup of the symlinked ring completed'); + unlike( + $err, + qr/WARNING|skipping special file/, 'no warnings for the symlinked pg_dwb'); - ok(-d "$link_backup/pg_dwb" && !-l "$link_backup/pg_dwb", + ok( -d "$link_backup/pg_dwb" && !-l "$link_backup/pg_dwb", 'symlinked pg_dwb became a real directory in the backup'); opendir(my $dh, "$link_backup/pg_dwb") or die "opendir: $!"; my @entries = grep { !/^\.\.?$/ } readdir($dh); diff --git a/src/test/modules/test_dwb/t/007_rewind.pl b/src/test/modules/test_dwb/t/007_rewind.pl index cd23c50a67671..9b6e30031128a 100644 --- a/src/test/modules/test_dwb/t/007_rewind.pl +++ b/src/test/modules/test_dwb/t/007_rewind.pl @@ -27,7 +27,8 @@ wal_keep_size = 64MB )); $node_a->start; -$node_a->safe_psql('postgres', q( +$node_a->safe_psql( + 'postgres', q( CREATE TABLE dwb_r AS SELECT g AS id FROM generate_series(1, 100) g; )); @@ -80,7 +81,7 @@ # leave proof on the target that the rewind, not a later cold start, # removed the ring files -ok(-f $node_a->data_dir . '/pg_dwb/control', +ok( -f $node_a->data_dir . '/pg_dwb/control', 'the target has ring files before the rewind'); $node_b->stop('fast'); @@ -185,7 +186,8 @@ command_ok([@rewind_from_b], 'pg_rewind from a stopped source succeeds'); -ok(-d $node_a->data_dir . '/pg_dwb', 'the target still has a pg_dwb directory'); +ok(-d $node_a->data_dir . '/pg_dwb', + 'the target still has a pg_dwb directory'); { opendir(my $dh, $node_a->data_dir . '/pg_dwb') or die "opendir: $!"; my @entries = grep { !/^\.\.?$/ } readdir($dh); @@ -215,13 +217,15 @@ 'rewound node cold-started a fresh ring'); $node_b->wait_for_catchup($node_a); -is( $node_a->safe_psql('postgres', 'SELECT count(*) FROM dwb_r'), +is($node_a->safe_psql('postgres', 'SELECT count(*) FROM dwb_r'), '101', 'rewound node converged on the new primary timeline'); -is( $node_a->safe_psql('postgres', - 'SELECT count(*) FROM dwb_r WHERE id = 200001'), - '0', 'the divergent row is gone'); -is( $node_a->safe_psql('postgres', - 'SELECT count(*) FROM dwb_r WHERE id = 100001'), - '1', "the new primary's row is present"); +is( $node_a->safe_psql( + 'postgres', 'SELECT count(*) FROM dwb_r WHERE id = 200001'), + '0', + 'the divergent row is gone'); +is( $node_a->safe_psql( + 'postgres', 'SELECT count(*) FROM dwb_r WHERE id = 100001'), + '1', + "the new primary's row is present"); done_testing(); diff --git a/src/test/modules/test_dwb/t/008_modes.pl b/src/test/modules/test_dwb/t/008_modes.pl index 9462279f5d2dd..b8974fb1611a2 100644 --- a/src/test/modules/test_dwb/t/008_modes.pl +++ b/src/test/modules/test_dwb/t/008_modes.pl @@ -25,8 +25,10 @@ sub wal_window my $lsn1 = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); my ($out, $err) = run_command( [ - 'pg_waldump', '--path' => $node->data_dir . '/pg_wal', - '--start' => $lsn0, '--end' => $lsn1 + 'pg_waldump', + '--path' => $node->data_dir . '/pg_wal', + '--start' => $lsn0, + '--end' => $lsn1 ]); is($err, '', "pg_waldump read the window of: $stmt"); return $out; @@ -37,11 +39,10 @@ sub wal_window $node->append_conf('postgresql.conf', 'io_torn_pages_protection = off'); my $log_offset = (-s $node->logfile) // 0; $node->start; -ok( $node->log_contains( - qr/torn page protection is disabled/, $log_offset), +ok($node->log_contains(qr/torn page protection is disabled/, $log_offset), 'mode "off" announces itself'); ok(!-d $node->data_dir . '/pg_dwb', 'mode "off" creates no ring'); -is( $node->safe_psql('postgres', 'SHOW full_page_writes'), +is($node->safe_psql('postgres', 'SHOW full_page_writes'), 'on', 'the legacy GUC still reads on...'); $node->safe_psql('postgres', @@ -53,7 +54,8 @@ sub wal_window # --- mode "full_pages": the legacy GUC keeps its vanilla meaning --------- -$node->append_conf('postgresql.conf', qq( +$node->append_conf( + 'postgresql.conf', qq( io_torn_pages_protection = full_pages full_page_writes = off )); @@ -71,7 +73,8 @@ sub wal_window # --- mode "double_writes": a SIGHUP of the legacy GUC is a no-op --------- -$node->append_conf('postgresql.conf', qq( +$node->append_conf( + 'postgresql.conf', qq( io_torn_pages_protection = double_writes full_page_writes = on )); @@ -92,8 +95,10 @@ sub wal_window my $lsn_end = $node->safe_psql('postgres', 'SELECT pg_current_wal_lsn()'); my ($out, $err) = run_command( [ - 'pg_waldump', '--path' => $node->data_dir . '/pg_wal', - '--start' => $reload_lsn, '--end' => $lsn_end + 'pg_waldump', + '--path' => $node->data_dir . '/pg_wal', + '--start' => $reload_lsn, + '--end' => $lsn_end ]); is($err, '', 'pg_waldump read the reload window cleanly'); unlike($out, qr/FPW_CHANGE/, @@ -106,7 +111,8 @@ sub wal_window # closed cleanly once. $node->stop('immediate'); -$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +$node->append_conf('postgresql.conf', + 'io_torn_pages_protection = full_pages'); my $ret = $node->start(fail_ok => 1); is($ret, 0, 'crashed ring refuses a full_pages start'); ok( $node->log_contains( @@ -123,14 +129,15 @@ sub wal_window 'io_torn_pages_protection = double_writes'); $log_offset = -s $node->logfile; $node->start; -ok( $node->log_contains(qr/double write buffer recovery:/, $log_offset), +ok($node->log_contains(qr/double write buffer recovery:/, $log_offset), 'the double_writes start runs the apply-pass'); $node->stop; -$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +$node->append_conf('postgresql.conf', + 'io_torn_pages_protection = full_pages'); $log_offset = -s $node->logfile; $node->start; -is( $node->safe_psql('postgres', 'SHOW io_torn_pages_protection'), +is($node->safe_psql('postgres', 'SHOW io_torn_pages_protection'), 'full_pages', 'after a clean stop the mode change is legal'); ok( !$node->log_contains(qr/ring opened/, $log_offset), '... and the leftover ring stays closed'); @@ -146,7 +153,8 @@ sub wal_window $node->start; $node->stop('immediate'); -$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +$node->append_conf('postgresql.conf', + 'io_torn_pages_protection = full_pages'); $ret = $node->start(fail_ok => 1); is($ret, 0, 'a crash after reopening the ring re-arms the guard'); @@ -162,7 +170,8 @@ sub wal_window # full_pages run (which touches neither the marker nor the generation) # must NOT re-arm the ring — an apply here would resurrect ancient # same-generation slots over pages torn long after the ring was closed. -$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +$node->append_conf('postgresql.conf', + 'io_torn_pages_protection = full_pages'); $node->start; $node->stop('immediate'); @@ -185,7 +194,8 @@ sub wal_window print $fh "\x00" x 16; close $fh; -$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +$node->append_conf('postgresql.conf', + 'io_torn_pages_protection = full_pages'); $log_offset = -s $node->logfile; $ret = $node->start(fail_ok => 1); is($ret, 0, 'a corrupt ring control refuses a full_pages start'); @@ -197,7 +207,7 @@ sub wal_window # the hint's recipe: removing pg_dwb unblocks the start rmtree($node->data_dir . '/pg_dwb'); $node->start; -is( $node->safe_psql('postgres', 'SHOW io_torn_pages_protection'), +is($node->safe_psql('postgres', 'SHOW io_torn_pages_protection'), 'full_pages', 'removing pg_dwb unblocks the non-ring mode'); # --- a crash under "off" is announced on the next protected start -------- @@ -209,7 +219,8 @@ sub wal_window $node->restart; $node->stop('immediate'); -$node->append_conf('postgresql.conf', 'io_torn_pages_protection = full_pages'); +$node->append_conf('postgresql.conf', + 'io_torn_pages_protection = full_pages'); $log_offset = -s $node->logfile; $node->start; ok( $node->log_contains( @@ -239,15 +250,14 @@ sub wal_window qr!FATAL: .* file "pg_dwb/control" requires format version at least 2, but this server supports 1!, $log_offset), '... naming the version gap'); -ok( !$node->log_contains(qr/could not be validated/, $log_offset), +ok(!$node->log_contains(qr/could not be validated/, $log_offset), '... and not the corrupt-ring advice'); # the intact-but-unreadable ring can only be resolved by removal rmtree($node->data_dir . '/pg_dwb'); $log_offset = -s $node->logfile; $node->start; -ok( $node->log_contains( - qr/ring opened: .* generation 1\b/, $log_offset), +ok( $node->log_contains(qr/ring opened: .* generation 1\b/, $log_offset), 'removing the newer ring unblocks a fresh double_writes start'); # --- leftovers of an interrupted wipe are swept, not fatal --------------- @@ -266,10 +276,9 @@ sub wal_window $log_offset = -s $node->logfile; $node->start; -ok( !$node->log_contains(qr/double write buffer recovery:/, $log_offset), +ok(!$node->log_contains(qr/double write buffer recovery:/, $log_offset), 'no apply-pass over the swept leftovers'); -ok( $node->log_contains( - qr/ring opened: .* generation 1\b/, $log_offset), +ok($node->log_contains(qr/ring opened: .* generation 1\b/, $log_offset), 'the interrupted-wipe state cold-starts a fresh ring'); ok(!-e $leftover, 'the leftover batch file is gone'); diff --git a/src/test/modules/test_dwb/t/009_fpw_transition.pl b/src/test/modules/test_dwb/t/009_fpw_transition.pl index 9f189d71a5722..8eca8558fe406 100644 --- a/src/test/modules/test_dwb/t/009_fpw_transition.pl +++ b/src/test/modules/test_dwb/t/009_fpw_transition.pl @@ -107,7 +107,8 @@ # --- a double_writes standby follows the same primary -------------------- -$standby->append_conf('postgresql.conf', qq( +$standby->append_conf( + 'postgresql.conf', qq( io_torn_pages_protection = double_writes dwb_num_batches = 16 dwb_batch_pages = 16 @@ -119,7 +120,7 @@ $ring_offset), 'reconfigured standby cold-starts a ring of its own'); $primary->wait_for_catchup($standby); -is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpw'), +is($standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpw'), '3000', 'and replays the image-less WAL'); # --- a crash of a double_writes standby of an "off" primary is quiet ------ @@ -138,11 +139,11 @@ my $warn_offset = -s $standby->logfile; $standby->start; ok( !$standby->log_contains( - qr/interrupted while torn page protection was disabled/, - $warn_offset), + qr/interrupted while torn page protection was disabled/, $warn_offset + ), 'crashed double_writes standby of an "off" primary draws no warning'); $primary->wait_for_catchup($standby); -is( $standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpw'), +is($standby->safe_psql('postgres', 'SELECT count(*) FROM dwb_fpw'), '4000', 'and keeps replaying'); done_testing(); diff --git a/src/test/modules/test_dwb/t/010_recovery.pl b/src/test/modules/test_dwb/t/010_recovery.pl index 038b084f627e1..d4e4afb88c039 100644 --- a/src/test/modules/test_dwb/t/010_recovery.pl +++ b/src/test/modules/test_dwb/t/010_recovery.pl @@ -44,17 +44,18 @@ )); $node->start; -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE thint AS SELECT g AS id FROM generate_series(1, 100) g; CREATE TABLE told AS SELECT g AS id FROM generate_series(1, 100) g; )); $node->safe_psql('postgres', 'CHECKPOINT'); my $thint_file = - $node->data_dir . '/' + $node->data_dir . '/' . $node->safe_psql('postgres', "SELECT pg_relation_filepath('thint')"); my $told_file = - $node->data_dir . '/' + $node->data_dir . '/' . $node->safe_psql('postgres', "SELECT pg_relation_filepath('told')"); my $thint_relnum = $node->safe_psql('postgres', "SELECT relfilenode FROM pg_class WHERE relname = 'thint'"); @@ -88,7 +89,7 @@ ok( $node->log_contains( qr/ring opened: 16 batches of 16 pages, generation 2\b/, $log_offset), 'generation bumped after the pass'); -is( $node->safe_psql('postgres', 'SELECT count(*) FROM thint'), +is($node->safe_psql('postgres', 'SELECT count(*) FROM thint'), '100', 'torn hint page is whole again'); # --- a checksum-valid but stale page is repaired by its LSN -------------- @@ -117,9 +118,9 @@ qr!restoring page 0 of relation \d+/\d+/$told_relnum fork 0!, $log_offset), '... by its LSN — the page verified fine'); -is( $node->safe_psql( - 'postgres', 'SELECT count(*) FROM told WHERE id > 1000'), - '50', 'stale page carries the update again'); +is( $node->safe_psql('postgres', 'SELECT count(*) FROM told WHERE id > 1000'), + '50', + 'stale page carries the update again'); # --- a repeated pass over the same ring is a no-op ----------------------- @@ -137,18 +138,19 @@ qr/double write buffer recovery: 0 of 1 candidate pages restored/, $log_offset), 're-applied pass sees the same candidate and rewrites nothing'); -is( $node->safe_psql( - 'postgres', 'SELECT count(*) FROM told WHERE id > 1000'), - '50', 'data intact after the repeated pass'); +is( $node->safe_psql('postgres', 'SELECT count(*) FROM told WHERE id > 1000'), + '50', + 'data intact after the repeated pass'); # --- a clean start skips the pass, but still bumps the generation -------- -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE tstale AS SELECT g AS id FROM generate_series(1, 100) g; )); $node->safe_psql('postgres', 'CHECKPOINT'); my $tstale_file = - $node->data_dir . '/' + $node->data_dir . '/' . $node->safe_psql('postgres', "SELECT pg_relation_filepath('tstale')"); $node->stop; @@ -156,7 +158,7 @@ $log_offset = -s $node->logfile; $node->start; -ok( !$node->log_contains(qr/double write buffer recovery:/, $log_offset), +ok(!$node->log_contains(qr/double write buffer recovery:/, $log_offset), 'clean start runs no apply-pass'); ok( $node->log_contains(qr/ring opened: .* generation 4\b/, $log_offset), '... yet the generation still moves, expiring the old slots'); @@ -175,12 +177,13 @@ qr/double write buffer recovery: 0 of 0 candidate pages restored/, $log_offset), 'no current-generation candidates after the idle crash'); -is( read_block($tstale_file, 0), chr(0xAB) x 8192, +is( read_block($tstale_file, 0), + chr(0xAB) x 8192, 'the stale slot was not applied to the corrupted page'); # put the good page back so the cluster winds down healthy write_block($tstale_file, 0, $tstale_good); -is( $node->safe_psql('postgres', 'SELECT count(*) FROM tstale'), +is($node->safe_psql('postgres', 'SELECT count(*) FROM tstale'), '100', 'page manually restored, cluster consistent'); # --- the marker alone triggers the pass, not the pg_control state -------- @@ -190,12 +193,13 @@ # Only the unset RING_CLEAN marker knows this ring was never retired — a # standby whose shutdown restartpoint was skipped leaves exactly this # combination, and the pass must key on the marker, not on pg_control. -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE tmark AS SELECT g AS id FROM generate_series(1, 100) g; )); $node->safe_psql('postgres', 'CHECKPOINT'); my $tmark_file = - $node->data_dir . '/' + $node->data_dir . '/' . $node->safe_psql('postgres', "SELECT pg_relation_filepath('tmark')"); $node->stop('immediate'); @@ -218,7 +222,7 @@ qr/double write buffer recovery: 1 of 1 candidate pages restored/, $log_offset), 'unretired ring is applied despite a clean pg_control'); -is( $node->safe_psql('postgres', 'SELECT count(*) FROM tmark'), +is($node->safe_psql('postgres', 'SELECT count(*) FROM tmark'), '100', 'torn page behind a clean shutdown is whole again'); # --- a slot for a dropped relation is skipped ---------------------------- @@ -226,7 +230,8 @@ # The relation's file may survive as an empty tombstone until the next # checkpoint, or be gone entirely; either way there is nothing to repair # and the pass must not trip over it. -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE tdrop AS SELECT g AS id FROM generate_series(1, 100) g; )); $node->safe_psql('postgres', 'CHECKPOINT'); @@ -246,12 +251,13 @@ # from its init record without reading it, and a stale slot must not # resurrect on it — the zeroed page's LSN 0 would lose the LSN comparison # that this skip protects. -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE tzero AS SELECT g AS id FROM generate_series(1, 100) g; )); $node->safe_psql('postgres', 'CHECKPOINT'); my $tzero_file = - $node->data_dir . '/' + $node->data_dir . '/' . $node->safe_psql('postgres', "SELECT pg_relation_filepath('tzero')"); $node->stop('immediate'); @@ -264,12 +270,13 @@ qr/double write buffer recovery: 0 of 1 candidate pages restored/, $log_offset), 'a zeroed page is not repaired from its slot'); -is( read_block($tzero_file, 0), "\0" x 8192, +is( read_block($tzero_file, 0), + "\0" x 8192, '... and stays zero for replay to drive'); # put the good page back so the cluster winds down healthy write_block($tzero_file, 0, $tzero_good); -is( $node->safe_psql('postgres', 'SELECT count(*) FROM tzero'), +is($node->safe_psql('postgres', 'SELECT count(*) FROM tzero'), '100', 'page manually restored, cluster consistent'); done_testing(); diff --git a/src/test/modules/test_dwb/t/011_geometry_recovery.pl b/src/test/modules/test_dwb/t/011_geometry_recovery.pl index cf4c96af7aa07..322ba13fb909e 100644 --- a/src/test/modules/test_dwb/t/011_geometry_recovery.pl +++ b/src/test/modules/test_dwb/t/011_geometry_recovery.pl @@ -37,12 +37,13 @@ )); $node->start; -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE tgeo AS SELECT g AS id FROM generate_series(1, 100) g; )); $node->safe_psql('postgres', 'CHECKPOINT'); my $tgeo_file = - $node->data_dir . '/' + $node->data_dir . '/' . $node->safe_psql('postgres', "SELECT pg_relation_filepath('tgeo')"); # stale-page damage: put the pre-update image back after the crash, so the @@ -69,6 +70,7 @@ qr/ring opened: 16 batches of 32 pages, generation 1\b/, $log_offset), '... with a fresh generation'); is( $node->safe_psql('postgres', 'SELECT count(*) FROM tgeo WHERE id > 1000'), - '50', 'the stale page carries the update again'); + '50', + 'the stale page carries the update again'); done_testing(); diff --git a/src/test/modules/test_dwb/t/012_apply_crafted.pl b/src/test/modules/test_dwb/t/012_apply_crafted.pl index b24246a9c19d3..2b167dd2271e1 100644 --- a/src/test/modules/test_dwb/t/012_apply_crafted.pl +++ b/src/test/modules/test_dwb/t/012_apply_crafted.pl @@ -37,17 +37,18 @@ $primary->start; $primary->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); -$primary->safe_psql('postgres', q( +$primary->safe_psql( + 'postgres', q( CREATE TABLE tlsn AS SELECT g AS id FROM generate_series(1, 100) g; CREATE TABLE ttie AS SELECT g AS id FROM generate_series(1, 100) g; )); $primary->safe_psql('postgres', 'CHECKPOINT'); my $tlsn_file = - $primary->data_dir . '/' + $primary->data_dir . '/' . $primary->safe_psql('postgres', "SELECT pg_relation_filepath('tlsn')"); my $ttie_file = - $primary->data_dir . '/' + $primary->data_dir . '/' . $primary->safe_psql('postgres', "SELECT pg_relation_filepath('ttie')"); my $tlsn_relnum = $primary->safe_psql('postgres', "SELECT relfilenode FROM pg_class WHERE relname = 'tlsn'"); @@ -71,7 +72,8 @@ # The LSN pair goes into batches 14/15. The tie pair goes into 12/13 with # the HIGHER batch_id in the LOWER batch index, so a comparator that merely # kept the later-scanned candidate would pick the wrong slot. -$primary->safe_psql('postgres', qq( +$primary->safe_psql( + 'postgres', qq( SELECT test_dwb_craft_batch(14, 501, $tlsn_relnum, 0, '$lsn_a', 'DWBLSNLOSER'); SELECT test_dwb_craft_batch(15, 502, $tlsn_relnum, 0, '$lsn_b', 'DWBLSNWINNER'); SELECT test_dwb_craft_batch(12, 601, $ttie_relnum, 0, '$insert_lsn', 'DWBTIEWINNER'); @@ -90,18 +92,19 @@ $log_offset), 'on equal LSNs the higher batch_id won, against scan order'); -like(read_block($tlsn_file, 0), qr/DWBLSNWINNER/, - 'winning image is on disk'); -unlike(read_block($tlsn_file, 0), qr/DWBLSNLOSER/, - '... and the losing image is not'); -like(read_block($ttie_file, 0), qr/DWBTIEWINNER/, - 'winning tie image is on disk'); -unlike(read_block($ttie_file, 0), qr/DWBTIELOSER/, - '... and the losing tie image is not'); +like(read_block($tlsn_file, 0), qr/DWBLSNWINNER/, 'winning image is on disk'); +unlike(read_block($tlsn_file, 0), + qr/DWBLSNLOSER/, '... and the losing image is not'); +like(read_block($ttie_file, 0), + qr/DWBTIEWINNER/, 'winning tie image is on disk'); +unlike(read_block($ttie_file, 0), + qr/DWBTIELOSER/, '... and the losing tie image is not'); is( $primary->safe_psql( - 'postgres', 'SELECT count(*) FROM tlsn UNION ALL SELECT count(*) FROM ttie'), - "100\n100", 'both repaired pages read back fine'); + 'postgres', + 'SELECT count(*) FROM tlsn UNION ALL SELECT count(*) FROM ttie'), + "100\n100", + 'both repaired pages read back fine'); # --- a crafted slot beyond minRecoveryPoint raises it on the standby ------ @@ -110,7 +113,8 @@ $standby->init_from_backup($primary, 'bkp', has_streaming => 1); $standby->start; -$primary->safe_psql('postgres', q( +$primary->safe_psql( + 'postgres', q( CREATE TABLE tmrp AS SELECT g AS id FROM generate_series(1, 100) g; )); $primary->safe_psql('postgres', 'CHECKPOINT'); @@ -120,7 +124,7 @@ my $tmrp_relnum = $standby->safe_psql('postgres', "SELECT relfilenode FROM pg_class WHERE relname = 'tmrp'"); my $tmrp_file = - $standby->data_dir . '/' + $standby->data_dir . '/' . $standby->safe_psql('postgres', "SELECT pg_relation_filepath('tmrp')"); # Hold replay while the primary moves ahead: the standby then holds @@ -134,7 +138,8 @@ $primary->wait_for_catchup($standby, 'flush', $mrp_lsn); $standby->safe_psql('postgres', - "SELECT test_dwb_craft_batch(15, 700, $tmrp_relnum, 0, '$mrp_lsn', 'DWBMRPMARK')"); + "SELECT test_dwb_craft_batch(15, 700, $tmrp_relnum, 0, '$mrp_lsn', 'DWBMRPMARK')" +); $standby->stop('immediate'); $log_offset = -s $standby->logfile; @@ -148,11 +153,11 @@ qr/raising minimum recovery point to $mrp_re to cover pages repaired from the double write buffer/, $log_offset), 'minimum recovery point raised to the applied LSN'); -like(read_block($tmrp_file, 0), qr/DWBMRPMARK/, - 'crafted image is on the standby disk'); +like(read_block($tmrp_file, 0), + qr/DWBMRPMARK/, 'crafted image is on the standby disk'); $primary->wait_for_catchup($standby); -is( $standby->safe_psql('postgres', 'SELECT count(*) FROM tmrp'), +is($standby->safe_psql('postgres', 'SELECT count(*) FROM tmrp'), '100', 'standby reads the repaired page fine'); done_testing(); diff --git a/src/test/modules/test_dwb/t/013_backup_start_point.pl b/src/test/modules/test_dwb/t/013_backup_start_point.pl index 2f3a6946c6ec7..0ab645ae79911 100644 --- a/src/test/modules/test_dwb/t/013_backup_start_point.pl +++ b/src/test/modules/test_dwb/t/013_backup_start_point.pl @@ -32,7 +32,8 @@ )); $node->start; -$node->safe_psql('postgres', q( +$node->safe_psql( + 'postgres', q( CREATE TABLE dwb_bsp AS SELECT g AS id FROM generate_series(1, 100) g; )); @@ -74,14 +75,15 @@ qr/discarding double write buffer ring contents restored from a base backup/, $log_offset), 'the label start discarded the copied ring'); -ok( !$restored->log_contains( - qr/double write buffer recovery:/, $log_offset), +ok( !$restored->log_contains(qr/double write buffer recovery:/, $log_offset), '... and ran no apply-pass'); ok(!-f "$pgdata/backup_label", 'the failed recovery consumed backup_label'); ok(-f "$pgdata/backup_label.old", '... renaming it out of the way'); my ($cd, $cderr) = run_command([ 'pg_controldata', $pgdata ]); -like($cd, qr/Backup start location:\s+(?!0\/0)\S/, +like( + $cd, + qr/Backup start location:\s+(?!0\/0)\S/, 'pg_control still carries backupStartPoint'); # --- second start: the backupStartPoint arm ------------------------------ @@ -96,8 +98,7 @@ qr/discarding double write buffer ring contents restored from a base backup/, $log_offset), 'backupStartPoint alone still discards the ring'); -ok( !$restored->log_contains( - qr/double write buffer recovery:/, $log_offset), +ok( !$restored->log_contains(qr/double write buffer recovery:/, $log_offset), '... and no apply-pass ran on the second start either'); done_testing(); diff --git a/src/test/modules/test_dwb/t/014_pg_upgrade.pl b/src/test/modules/test_dwb/t/014_pg_upgrade.pl index ca827270df18f..5f076674d1558 100644 --- a/src/test/modules/test_dwb/t/014_pg_upgrade.pl +++ b/src/test/modules/test_dwb/t/014_pg_upgrade.pl @@ -26,12 +26,13 @@ $old->init; $old->append_conf('postgresql.conf', $dwb_conf); $old->start; -$old->safe_psql('postgres', q( +$old->safe_psql( + 'postgres', q( CREATE TABLE dwb_up AS SELECT g AS id FROM generate_series(1, 100) g; )); $old->safe_psql('postgres', 'CHECKPOINT'); $old->stop; -ok(-f $old->data_dir . '/pg_dwb/control', +ok( -f $old->data_dir . '/pg_dwb/control', 'the old cluster leaves a ring behind'); my $new = PostgreSQL::Test::Cluster->new('dwb_upgrade_new'); @@ -51,7 +52,7 @@ ], 'pg_upgrade from a double_writes cluster succeeds'); -ok(-f $old->data_dir . '/pg_dwb/control', +ok( -f $old->data_dir . '/pg_dwb/control', 'the old ring stays with the old cluster'); ok(!-d $new->data_dir . '/pg_dwb', 'nothing of the ring was shipped into the new cluster'); @@ -64,7 +65,7 @@ qr/double write buffer ring opened: 16 batches of 16 pages, generation 1\b/, $log_offset), 'the upgraded cluster cold-starts a fresh ring'); -is( $new->safe_psql('postgres', 'SELECT count(*) FROM dwb_up'), +is($new->safe_psql('postgres', 'SELECT count(*) FROM dwb_up'), '100', 'the upgraded data is intact'); done_testing(); From c9c17c5f4d4281b16b3923ac742d86a6042bb933 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 1 Aug 2026 14:47:04 +0300 Subject: [PATCH 22/52] Vectorize the checkpoint flush and slice the ring reserves (Stage 5) The 6-point grid on the SF75K stand showed two structural sink defects: the checkpointer degenerated to one-slot batches (one fdatasync per page via the lone-writer seal), and at >= 2700 connections it starved outright because the one-sided eviction reserve never let the background class open a batch while evicting backends kept the free count at the throttle line. BufferSync now collects permanent buffers into bins of up to dwb_batch_pages (capped at 64 by the content-lock budget) and flushes each bin through the DWB as one batch: a non-blocking gather (conditional content lock plus nowait StartBufferIO; contended buffers fall back to the per-page path), one WAL flush, one staged batch with a single fdatasync, then the data-file writes. New DWBStagePageWriteNoWait and DWBWaitStagedWrites compose the existing write-path primitives for vectored callers. The FREE-batch policy becomes sliced: the bottom DWB_BG_RESERVE batches are for the background class only, the middle DWB_EVICT_RESERVE slice for eviction only, everything above is shared. A leave-some-behind rule alone cannot end the starvation: under saturation the free count hovers at the greedier class's throttle line and the background stream never reaches a batch. test_dwb: 001 covers the sliced boundaries including the background lane at free=1; 003 fills the background lane before the checkpointer stall scenario; 010/011 accept the larger candidate sets of bin-written rings; new 015_vectored_flush.pl pins the multi-slot batch ratio via pg_stat_io and repairs a torn page written by the vectored path. --- src/backend/storage/buffer/bufmgr.c | 261 +++++++++++++++++- src/backend/storage/dwb/dwb.c | 64 ++++- src/include/storage/dwb.h | 15 +- src/test/modules/test_dwb/meson.build | 1 + src/test/modules/test_dwb/t/001_dwb.pl | 21 +- .../modules/test_dwb/t/003_backpressure.pl | 6 + src/test/modules/test_dwb/t/010_recovery.pl | 12 +- .../test_dwb/t/011_geometry_recovery.pl | 2 +- .../modules/test_dwb/t/015_vectored_flush.pl | 102 +++++++ src/test/modules/test_dwb/test_dwb.c | 18 +- 10 files changed, 471 insertions(+), 31 deletions(-) create mode 100644 src/test/modules/test_dwb/t/015_vectored_flush.pl diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d3827f712fc0f..01303f373b2bf 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/checksum.h" #include "storage/dwb.h" #include "storage/fd.h" #include "storage/ipc.h" @@ -63,6 +64,7 @@ #include "storage/smgr.h" #include "storage/standby.h" #include "utils/memdebug.h" +#include "utils/memutils.h" #include "utils/ps_status.h" #include "utils/rel.h" #include "utils/resowner.h" @@ -81,6 +83,15 @@ #define BUF_WRITTEN 0x01 #define BUF_REUSABLE 0x02 +/* + * Bin size cap for the vectored checkpoint flush (FlushCkptBufferBin): the + * flush holds a pin, a shared content lock and BM_IO_IN_PROGRESS per bin + * member at once, so the cap must leave MAX_SIMUL_LWLOCKS (200) plenty of + * headroom. 64 matches the default dwb_batch_pages; larger batch_pages + * settings seal their batches at bin-sized fills. + */ +#define CKPT_DWB_BIN_MAX 64 + #define RELS_BSEARCH_THRESHOLD 20 /* @@ -519,6 +530,8 @@ static void UnpinBuffer(BufferDesc *buf); static void UnpinBufferNoOwner(BufferDesc *buf); static void BufferSync(int flags); static uint32 WaitBufHdrUnlocked(BufferDesc *buf); +static int FlushCkptBufferBin(const int *buf_ids, int nbuf, + WritebackContext *wb_context); static int SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context); static void WaitIO(BufferDesc *buf); @@ -3365,6 +3378,9 @@ BufferSync(int flags) int i; int mask = BM_DIRTY; WritebackContext wb_context; + int *dwb_bin = NULL; + int dwb_bin_n = 0; + int dwb_bin_size = 0; /* * Unless this is a shutdown checkpoint or we have been explicitly told, @@ -3428,6 +3444,17 @@ BufferSync(int flags) WritebackContextInit(&wb_context, &checkpoint_flush_after); + /* + * With the double write buffer active, permanent buffers are flushed in + * bins of up to a batch: one batch write and one fdatasync cover the + * whole bin instead of one per page (see FlushCkptBufferBin). + */ + if (DWBIsEnabled() && !IsBootstrapProcessingMode()) + { + dwb_bin_size = Min(dwb_batch_pages, CKPT_DWB_BIN_MAX); + dwb_bin = palloc(dwb_bin_size * sizeof(int)); + } + TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_scan); /* @@ -3561,7 +3588,27 @@ BufferSync(int flags) */ if (pg_atomic_read_u32(&bufHdr->state) & BM_CHECKPOINT_NEEDED) { - if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN) + if (dwb_bin != NULL && + (pg_atomic_read_u32(&bufHdr->state) & BM_PERMANENT)) + { + /* + * Vectored flush: collect permanent buffers into a bin and + * write them through the double write buffer with a single + * fdatasync. Nothing is locked while the bin fills; the + * members are re-checked when it flushes. + */ + dwb_bin[dwb_bin_n++] = buf_id; + if (dwb_bin_n == dwb_bin_size) + { + int nw = FlushCkptBufferBin(dwb_bin, dwb_bin_n, + &wb_context); + + PendingCheckpointerStats.buffers_written += nw; + num_written += nw; + dwb_bin_n = 0; + } + } + else if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN) { TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id); PendingCheckpointerStats.buffers_written++; @@ -3596,6 +3643,20 @@ BufferSync(int flags) CheckpointWriteDelay(flags, (double) num_processed / num_to_scan); } + /* flush the residual bin of the vectored path */ + if (dwb_bin != NULL) + { + if (dwb_bin_n > 0) + { + int nw = FlushCkptBufferBin(dwb_bin, dwb_bin_n, + &wb_context); + + PendingCheckpointerStats.buffers_written += nw; + num_written += nw; + } + pfree(dwb_bin); + } + /* * Issue all pending flushes. Only checkpointer calls BufferSync(), so * IOContext will always be IOCONTEXT_NORMAL. @@ -3990,6 +4051,204 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context) return result | BUF_WRITTEN; } +/* + * FlushCkptBufferBin -- flush a bin of checkpoint buffers through the + * double write buffer as one batch. + * + * The per-page write protocol cannot amortize the batch fdatasync for a + * sequential stream: each page waits for its own batch copy to become + * durable before its data-file write, and the lone-writer seal then closes + * the batch over that single page — a checkpoint would pay one fdatasync + * per page. Here the whole bin is staged first, sealed and fdatasynced + * once, and only then written to the data files (the vectored background + * flush of the design, 3.4). + * + * All lock acquisitions in the gather phase are non-blocking: waiting for a + * content lock or for somebody's buffer I/O while already holding shared + * content locks of earlier bin members could deadlock against backends that + * take multiple buffer locks in their own order. Buffers that cannot be + * claimed without waiting fall back to the ordinary per-page SyncOneBuffer + * path after the bin is done, when nothing is held. + * + * Caller guarantees every buffer is BM_PERMANENT (non-permanent checkpoint + * buffers take the per-page path). Returns the number of buffers written. + */ +static int +FlushCkptBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) +{ + static char *bin_buf = NULL; + + BufferDesc *bufs[CKPT_DWB_BIN_MAX]; + XLogRecPtr lsns[CKPT_DWB_BIN_MAX]; + DWBSlotRef refs[CKPT_DWB_BIN_MAX]; + int fb_ids[CKPT_DWB_BIN_MAX]; + int gathered = 0; + int nfallback = 0; + int written = 0; + XLogRecPtr max_lsn = InvalidXLogRecPtr; + ErrorContextCallback errcallback; + + Assert(nbuf > 0 && nbuf <= CKPT_DWB_BIN_MAX); + + if (bin_buf == NULL) + bin_buf = MemoryContextAllocAligned(TopMemoryContext, + (Size) CKPT_DWB_BIN_MAX * BLCKSZ, + PG_IO_ALIGN_SIZE, 0); + + /* Phase 1: claim and copy what can be claimed without waiting */ + for (int i = 0; i < nbuf; i++) + { + BufferDesc *bufHdr = GetBufferDescriptor(buf_ids[i]); + uint32 buf_state; + char *dst; + + /* Make sure we can handle the pin */ + ReservePrivateRefCountEntry(); + ResourceOwnerEnlarge(CurrentResourceOwner); + + buf_state = LockBufHdr(bufHdr); + if (!(buf_state & BM_VALID) || !(buf_state & BM_DIRTY)) + { + /* clean already: nothing to do */ + UnlockBufHdr(bufHdr, buf_state); + continue; + } + Assert(buf_state & BM_PERMANENT); + PinBuffer_Locked(bufHdr); + + if (!LWLockConditionalAcquire(BufferDescriptorGetContentLock(bufHdr), + LW_SHARED)) + { + UnpinBuffer(bufHdr); + fb_ids[nfallback++] = buf_ids[i]; + continue; + } + if (!StartBufferIO(bufHdr, false, true)) + { + /* + * Either somebody else's I/O is in flight (fall back per-page: + * SyncOneBuffer may wait and rechecks dirtiness) or the buffer + * went clean; the fallback handles both. + */ + LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); + UnpinBuffer(bufHdr); + fb_ids[nfallback++] = buf_ids[i]; + continue; + } + + /* as in FlushBuffer: read the LSN under the header lock */ + buf_state = LockBufHdr(bufHdr); + lsns[gathered] = BufferGetLSN(bufHdr); + buf_state &= ~BM_JUST_DIRTIED; + UnlockBufHdr(bufHdr, buf_state); + + TRACE_POSTGRESQL_BUFFER_FLUSH_START(BufTagGetForkNum(&bufHdr->tag), + bufHdr->tag.blockNum, + BufTagGetRelFileLocator(&bufHdr->tag).spcOid, + BufTagGetRelFileLocator(&bufHdr->tag).dbOid, + BufTagGetRelFileLocator(&bufHdr->tag).relNumber); + + /* + * The private copy decouples the image from concurrent hint-bit + * updates, like PageSetChecksumCopy in the per-page path; an all-zero + * page must stay all-zero, so it gets no checksum. + */ + dst = bin_buf + (Size) gathered * BLCKSZ; + memcpy(dst, BufHdrGetBlock(bufHdr), BLCKSZ); + if (DataChecksumsEnabled() && !PageIsNew((Page) dst)) + ((PageHeader) dst)->pd_checksum = + pg_checksum_page(dst, bufHdr->tag.blockNum); + + if (lsns[gathered] > max_lsn) + max_lsn = lsns[gathered]; + bufs[gathered] = bufHdr; + gathered++; + } + + if (gathered > 0) + { + /* Phase 2: one WAL flush covers the whole bin (WAL before data) */ + if (!XLogRecPtrIsInvalid(max_lsn)) + XLogFlush(max_lsn); + + /* Setup error traceback support for ereport() */ + errcallback.callback = shared_buffer_write_error_callback; + errcallback.arg = NULL; + errcallback.previous = error_context_stack; + error_context_stack = &errcallback; + + /* Phase 3: stage everything, then one seal + one fdatasync */ + for (int i = 0; i < gathered; i++) + { + errcallback.arg = bufs[i]; + DWBStagePageWriteNoWait(&bufs[i]->tag, + bin_buf + (Size) i * BLCKSZ, + lsns[i], &refs[i]); + } + errcallback.arg = NULL; + DWBWaitStagedWrites(refs, gathered); + + /* Phase 4: the data-file writes */ + for (int i = 0; i < gathered; i++) + { + BufferDesc *bufHdr = bufs[i]; + SMgrRelation reln; + instr_time io_start; + BufferTag tag; + + errcallback.arg = bufHdr; + reln = smgropen(BufTagGetRelFileLocator(&bufHdr->tag), + INVALID_PROC_NUMBER); + + io_start = pgstat_prepare_io_time(track_io_timing); + smgrwrite(reln, + BufTagGetForkNum(&bufHdr->tag), + bufHdr->tag.blockNum, + bin_buf + (Size) i * BLCKSZ, + false); + pgstat_count_io_op_time(IOOBJECT_RELATION, IOCONTEXT_NORMAL, + IOOP_WRITE, io_start, 1, BLCKSZ); + + if (dwb_writeback) + smgrwriteback(reln, BufTagGetForkNum(&bufHdr->tag), + bufHdr->tag.blockNum, 1); + DWBFinishPageWrite(&refs[i]); + + pgBufferUsage.shared_blks_written++; + + TerminateBufferIO(bufHdr, true, 0, true, false); + + TRACE_POSTGRESQL_BUFFER_FLUSH_DONE(BufTagGetForkNum(&bufHdr->tag), + bufHdr->tag.blockNum, + BufTagGetRelFileLocator(&bufHdr->tag).spcOid, + BufTagGetRelFileLocator(&bufHdr->tag).dbOid, + BufTagGetRelFileLocator(&bufHdr->tag).relNumber); + + LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); + tag = bufHdr->tag; + TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(bufHdr->buf_id); + UnpinBuffer(bufHdr); + ScheduleBufferTagForWriteback(wb_context, IOCONTEXT_NORMAL, &tag); + + written++; + } + + error_context_stack = errcallback.previous; + } + + /* Phase 5: per-page fallback for the contended buffers, nothing held */ + for (int i = 0; i < nfallback; i++) + { + if (SyncOneBuffer(fb_ids[i], false, wb_context) & BUF_WRITTEN) + { + TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(fb_ids[i]); + written++; + } + } + + return written; +} + /* * AtEOXact_Buffers - clean up at end of transaction. * diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 077cc3134b0c8..212691bbdccd8 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -332,18 +332,26 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) } /* - * Count FREE batches first: a background-class open must leave - * DWB_EVICT_RESERVE of them for user evictions, so that a - * checkpoint's BufferSync storm cannot eat the ring from under - * latency-critical paths. FREE->ALLOCATED happens only under - * DWBRingOpenLock, and concurrent retirements only grow the count, so - * the check cannot overestimate. + * Count FREE batches first and apply the sliced reserves (see dwb.h): + * the eviction class may not consume the bottom DWB_BG_RESERVE + * batches, and the background class skips the middle + * DWB_EVICT_RESERVE slice — it opens either above both slices or + * inside its own bottom one. Leaving a slice to the other class + * alone would not be enough: under saturation the free count hovers + * at the throttle line of the greedier class, and a rule that only + * says "leave some behind" never lets the background stream reach a + * batch at all. FREE->ALLOCATED happens only under DWBRingOpenLock, + * and concurrent retirements only grow the count, so the check cannot + * overestimate. */ for (int i = 0; i < dwb_num_batches; i++) if (pg_atomic_read_u32(&DWBCtl->batches[i].state) == DWB_FREE) nfree++; - if (nfree > (wclass == DWB_WCLASS_BACKGROUND ? DWB_EVICT_RESERVE : 0)) + if (wclass == DWB_WCLASS_BACKGROUND ? + (nfree >= 1 && (nfree > DWB_BG_RESERVE + DWB_EVICT_RESERVE || + nfree <= DWB_BG_RESERVE)) : + nfree > DWB_BG_RESERVE) { for (int i = 0; i < dwb_num_batches; i++) { @@ -962,6 +970,48 @@ DWBStagePageWrite(const BufferTag *tag, const char *image, INJECTION_POINT("dwb-after-batch-fsynced", NULL); } +/* + * Steps 3-4 for a vectored caller: reserve and publish without waiting for + * durability. A sequential stream gets no rendezvous from the per-page + * protocol — each page would seal and fdatasync a batch of its own — so the + * background flushers stage a whole bin of pages first and then make them + * durable in one place with DWBWaitStagedWrites (one batch write and one + * fdatasync per bin; see "vectored background flush" in 3.4 of the design). + */ +void +DWBStagePageWriteNoWait(const BufferTag *tag, const char *image, + XLogRecPtr page_lsn, DWBSlotRef *ref) +{ + Assert(DWBCtl->ring_generation > 0); + + DWBAcquireSlot(tag, DWBWriterClass(), true, ref); + DWBPublishImage(ref, image, page_lsn); +} + +/* + * Step 5 for a vectored caller: seal every batch the bin's slots landed in + * and wait until they are all durable. On return the caller may write the + * staged copies to the data files. + * + * Slots were acquired in order and a batch held by our refs cannot recycle, + * so slots of the same batch are consecutive and comparing with the previous + * ref finds every batch boundary (normally none: one bin, one batch). + */ +void +DWBWaitStagedWrites(const DWBSlotRef *refs, int nrefs) +{ + for (int i = 0; i < nrefs; i++) + if (i == 0 || refs[i].batch_idx != refs[i - 1].batch_idx) + (void) DWBTrySealBatch(refs[i].batch_idx); + + for (int i = 0; i < nrefs; i++) + if (i == 0 || refs[i].batch_idx != refs[i - 1].batch_idx) + DWBWaitBatchFsynced(&refs[i]); + + if (nrefs > 0) + INJECTION_POINT("dwb-after-batch-fsynced", NULL); +} + /* * Step 7: release the ref after smgrwrite returned. Without a worker * pool, also retire synchronously so the ring keeps circulating (and, in diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 8b6c7957d530e..381c6b6cb47be 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -256,10 +256,18 @@ typedef struct DWSegEntry #define DWBSegBitmapWords() (((uint32) dwb_num_batches + 63) / 64) /* - * FREE batches held back from background-class opens so that a checkpoint's - * BufferSync storm can never eat the whole ring from under user evictions. + * Sliced reserves of FREE batches, by the free count F at open time: the + * bottom slice [1 .. DWB_BG_RESERVE] may be opened only by the background + * class, the middle slice (.. DWB_BG_RESERVE + DWB_EVICT_RESERVE] only by + * the eviction class, anything above by both. The middle slice keeps a + * checkpoint's BufferSync storm from eating the ring from under user + * evictions; the bottom slice keeps a crowd of evicting backends from + * starving the checkpointer outright (each class needs just one open batch, + * so a non-empty bottom slice is a progress guarantee for the background + * stream). */ #define DWB_EVICT_RESERVE Max(2, dwb_num_batches / 8) +#define DWB_BG_RESERVE Max(1, dwb_num_batches / 32) /* * next_slot_idx encoding: 30-bit index + writer-class bit + seal sentinel. @@ -366,6 +374,9 @@ extern void DWBShmemInit(void); /* dwb.c — write path */ extern void DWBStagePageWrite(const BufferTag *tag, const char *image, XLogRecPtr page_lsn, DWBSlotRef *ref); +extern void DWBStagePageWriteNoWait(const BufferTag *tag, const char *image, + XLogRecPtr page_lsn, DWBSlotRef *ref); +extern void DWBWaitStagedWrites(const DWBSlotRef *refs, int nrefs); extern void DWBFinishPageWrite(const DWBSlotRef *ref); extern bool DWBWritesPaused(void); extern void DWBAcquireSlot(const BufferTag *tag, int wclass, diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index a9074801fdcfb..fb0a591ac129e 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -51,6 +51,7 @@ tests += { 't/012_apply_crafted.pl', 't/013_backup_start_point.pl', 't/014_pg_upgrade.pl', + 't/015_vectored_flush.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/001_dwb.pl b/src/test/modules/test_dwb/t/001_dwb.pl index ba1c43547292a..0285752e60935 100644 --- a/src/test/modules/test_dwb/t/001_dwb.pl +++ b/src/test/modules/test_dwb/t/001_dwb.pl @@ -207,20 +207,27 @@ sub flip_byte is($node->safe_psql('postgres', 'SELECT count(*) FROM dwb_repair'), '100', 'torn block repaired from the batch copy (checksum-clean read)'); -# --- background writers leave the eviction reserve ----------------------- - -# DWB_EVICT_RESERVE = Max(2, 16/8) = 2 on this geometry: a background-class -# writer must stop opening batches once only the reserve is left, while an -# eviction-class writer may take the ring down to zero. +# --- the sliced reserves shape who may open what -------------------------- + +# On this geometry DWB_BG_RESERVE = Max(1, 16/32) = 1 and DWB_EVICT_RESERVE +# = Max(2, 16/8) = 2: a background-class writer filling a fresh ring stops +# above the middle eviction slice, an eviction-class writer consumes +# everything but the bottom background slice, and the background class can +# still open that last batch — the starvation-proof lane of the +# checkpointer. $bg = $node->background_psql('postgres'); my $bg_taken = $bg->query_safe('SELECT test_dwb_fill_ring(true)'); cmp_ok($bg_taken, '>', 0, 'background class filled the ring'); like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), - qr/free=2 /, 'background class stops at DWB_EVICT_RESERVE free batches'); + qr/free=3 /, 'background fill stops above the eviction slice'); my $ev_taken = $bg->query_safe('SELECT test_dwb_fill_ring(false)'); cmp_ok($ev_taken, '>', 0, 'eviction class still opens batches'); like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), - qr/free=0 /, 'eviction class may take the ring to zero'); + qr/free=1 /, 'eviction leaves the bottom background slice'); +my $lane_taken = $bg->query_safe('SELECT test_dwb_fill_ring(true)'); +cmp_ok($lane_taken, '>', 0, 'background class opens its reserved lane'); +like($node->safe_psql('postgres', 'SELECT test_dwb_states()'), + qr/free=0 /, '... consuming the ring fully'); $bg->quit; $node->poll_query_until('postgres', "SELECT CASE WHEN test_dwb_force_seal(false) IS NOT NULL THEN " diff --git a/src/test/modules/test_dwb/t/003_backpressure.pl b/src/test/modules/test_dwb/t/003_backpressure.pl index a674254ebff10..f51a297d234a2 100644 --- a/src/test/modules/test_dwb/t/003_backpressure.pl +++ b/src/test/modules/test_dwb/t/003_backpressure.pl @@ -106,6 +106,12 @@ $taken = $filler->query_safe('SELECT test_dwb_fill_ring()'); cmp_ok($taken, '>', 0, 'ring exhausted again for the checkpointer scenario'); +# An eviction fill stops at the bottom background slice, which is exactly +# the checkpointer's guaranteed lane — consume it too, or the checkpoint +# below would simply proceed through it instead of stalling. +$taken = $filler->query_safe('SELECT test_dwb_fill_ring(true)'); +cmp_ok($taken, '>', 0, 'the background lane is consumed as well'); + $node->safe_psql('postgres', "SELECT injection_points_attach('dwb-force-stall', 'notice')"); diff --git a/src/test/modules/test_dwb/t/010_recovery.pl b/src/test/modules/test_dwb/t/010_recovery.pl index d4e4afb88c039..e4ff42ee65ed7 100644 --- a/src/test/modules/test_dwb/t/010_recovery.pl +++ b/src/test/modules/test_dwb/t/010_recovery.pl @@ -79,7 +79,7 @@ my $log_offset = -s $node->logfile; $node->start; ok( $node->log_contains( - qr/double write buffer recovery: 1 of 1 candidate pages restored/, + qr/double write buffer recovery: 1 of \d+ candidate pages restored/, $log_offset), 'apply-pass restored the torn page'); ok( $node->log_contains( @@ -111,7 +111,7 @@ $log_offset = -s $node->logfile; $node->start; ok( $node->log_contains( - qr/double write buffer recovery: 1 of 1 candidate pages restored/, + qr/double write buffer recovery: 1 of \d+ candidate pages restored/, $log_offset), 'apply-pass restored the stale page'); ok( $node->log_contains( @@ -135,7 +135,7 @@ $log_offset = -s $node->logfile; $node->start; ok( $node->log_contains( - qr/double write buffer recovery: 0 of 1 candidate pages restored/, + qr/double write buffer recovery: 0 of \d+ candidate pages restored/, $log_offset), 're-applied pass sees the same candidate and rewrites nothing'); is( $node->safe_psql('postgres', 'SELECT count(*) FROM told WHERE id > 1000'), @@ -219,7 +219,7 @@ $log_offset = -s $node->logfile; $node->start; ok( $node->log_contains( - qr/double write buffer recovery: 1 of 1 candidate pages restored/, + qr/double write buffer recovery: 1 of \d+ candidate pages restored/, $log_offset), 'unretired ring is applied despite a clean pg_control'); is($node->safe_psql('postgres', 'SELECT count(*) FROM tmark'), @@ -241,7 +241,7 @@ $log_offset = -s $node->logfile; $node->start; ok( $node->log_contains( - qr/double write buffer recovery: 0 of 1 candidate pages restored/, + qr/double write buffer recovery: 0 of \d+ candidate pages restored/, $log_offset), 'a candidate for a dropped relation is counted but skipped'); @@ -267,7 +267,7 @@ $log_offset = -s $node->logfile; $node->start; ok( $node->log_contains( - qr/double write buffer recovery: 0 of 1 candidate pages restored/, + qr/double write buffer recovery: 0 of \d+ candidate pages restored/, $log_offset), 'a zeroed page is not repaired from its slot'); is( read_block($tzero_file, 0), diff --git a/src/test/modules/test_dwb/t/011_geometry_recovery.pl b/src/test/modules/test_dwb/t/011_geometry_recovery.pl index 322ba13fb909e..e1cef37ff9a74 100644 --- a/src/test/modules/test_dwb/t/011_geometry_recovery.pl +++ b/src/test/modules/test_dwb/t/011_geometry_recovery.pl @@ -59,7 +59,7 @@ my $log_offset = -s $node->logfile; $node->start; ok( $node->log_contains( - qr/double write buffer recovery: 1 of 1 candidate pages restored/, + qr/double write buffer recovery: 1 of \d+ candidate pages restored/, $log_offset), 'the crashed ring is applied with its recorded geometry'); ok( $node->log_contains( diff --git a/src/test/modules/test_dwb/t/015_vectored_flush.pl b/src/test/modules/test_dwb/t/015_vectored_flush.pl new file mode 100644 index 0000000000000..de05b0f9aeda6 --- /dev/null +++ b/src/test/modules/test_dwb/t/015_vectored_flush.pl @@ -0,0 +1,102 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# The vectored background flush: a checkpoint writes its buffers through +# the double write buffer in bins — one batch write and one fdatasync per +# bin instead of one per page. pg_stat_io proves the batching (the dwb +# "writes" counter is per batch, so write_bytes/writes is the batch size), +# and a torn page written by the vectored path is restored by the +# apply-pass like any other. + +use strict; +use warnings FATAL => 'all'; +use FindBin; +use lib $FindBin::RealBin; +use DWBTest; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_vectored'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 1 +autovacuum = off +bgwriter_lru_maxpages = 0 +log_min_messages = debug1 +)); +$node->start; + +# ~200 heap pages, all dirty: enough for a dozen full bins +$node->safe_psql( + 'postgres', q( + CREATE TABLE dwb_vec (id int, pad text) WITH (fillfactor = 10); + INSERT INTO dwb_vec SELECT g, repeat('v', 256) FROM generate_series(1, 1000) g; +)); +$node->safe_psql('postgres', 'CHECKPOINT'); +$node->safe_psql('postgres', "UPDATE dwb_vec SET pad = repeat('w', 256)"); +my $npages = + $node->safe_psql('postgres', "SELECT pg_relation_size('dwb_vec') / 8192"); +cmp_ok($npages, '>', 150, 'the table spans enough pages for full bins'); + +$node->safe_psql('postgres', 'CHECKPOINT'); + +# --- the checkpointer writes multi-slot batches --------------------------- + +# One dwb "write" is one batch write by its leader, so write_bytes/writes +# is the average batch size: the meta region (4096 bytes on this geometry) +# plus one 8 KB image per slot. The per-page protocol pins this ratio at +# exactly one slot for the checkpointer's sequential stream; the vectored +# flush must push it to bin-sized fills. Checkpointer stats reach the +# collector with a delay, so poll. +$node->poll_query_until( + 'postgres', q( + SELECT writes > 0 FROM pg_stat_io + WHERE backend_type = 'checkpointer' AND object = 'dwb' + AND context = 'normal' +)) or die 'timed out waiting for checkpointer dwb stats'; + +my $avg_slots = $node->safe_psql( + 'postgres', q( + SELECT round((write_bytes::numeric / writes - 4096) / 8192, 1) + FROM pg_stat_io + WHERE backend_type = 'checkpointer' AND object = 'dwb' + AND context = 'normal' +)); +cmp_ok($avg_slots, '>=', 4, + "checkpointer batches average $avg_slots slots, not one per page"); + +# --- a torn page of the vectored path is repaired ------------------------- + +# The last heap block sorts last in BufferSync, so it lands in the final +# bin and its slot survives any batch-index reuse by earlier bins. +my $vec_file = + $node->data_dir . '/' + . $node->safe_psql('postgres', "SELECT pg_relation_filepath('dwb_vec')"); +my $vec_relnum = $node->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE relname = 'dwb_vec'"); +my $sum_before = $node->safe_psql('postgres', 'SELECT sum(id) FROM dwb_vec'); +my $last_block = $npages - 1; + +$node->stop('immediate'); +write_block($vec_file, $last_block, + substr(read_block($vec_file, $last_block), 0, 4096) . ("\0" x 4096)); + +my $log_offset = -s $node->logfile; +$node->start; +ok( $node->log_contains( + qr/double write buffer recovery: 1 of \d+ candidate pages restored/, + $log_offset), + 'apply-pass restored the page torn under the vectored flush'); +ok( $node->log_contains( + qr!restoring page $last_block of relation \d+/\d+/$vec_relnum fork 0!, + $log_offset), + '... and it was the torn heap page'); +is($node->safe_psql('postgres', 'SELECT sum(id) FROM dwb_vec'), + $sum_before, 'data intact after the repair'); + +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index e83948d0378c7..30d0718c1f636 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -388,11 +388,11 @@ test_dwb_leak(PG_FUNCTION_ARGS) * Occupy the ring without blocking: acquire and publish slots until no * openable FREE batch remains and the open batch is full, keeping every ref * (the refs die with the session). Sets up ring exhaustion for the - * backpressure tests. With background = true the slots are taken in the - * BACKGROUND writer class, which must stop opening batches once only - * DWB_EVICT_RESERVE FREE ones are left. Meant for dwb_retire_workers = 0, - * where nothing seals or retires behind our back. Returns the number of - * slots taken. + * backpressure tests. The sliced reserves (dwb.h) shape where each class + * stops: an eviction fill may not consume the bottom DWB_BG_RESERVE FREE + * batches, a background fill from a fresh ring stops above the middle + * DWB_EVICT_RESERVE slice. Meant for dwb_retire_workers = 0, where nothing + * seals or retires behind our back. Returns the number of slots taken. */ PG_FUNCTION_INFO_V1(test_dwb_fill_ring); Datum @@ -400,7 +400,6 @@ test_dwb_fill_ring(PG_FUNCTION_ARGS) { bool background = PG_GETARG_BOOL(0); int wclass = background ? DWB_WCLASS_BACKGROUND : DWB_WCLASS_EVICTION; - int reserve = background ? DWB_EVICT_RESERVE : 0; int taken = 0; static char page[BLCKSZ]; @@ -409,6 +408,7 @@ test_dwb_fill_ring(PG_FUNCTION_ARGS) for (;;) { int nfree = 0; + bool can_open; uint32 open_idx; BufferTag tag; DWBSlotRef ref; @@ -422,8 +422,12 @@ test_dwb_fill_ring(PG_FUNCTION_ARGS) for (int i = 0; i < dwb_num_batches; i++) if (DWBGetBatchState(i) == DWB_FREE) nfree++; + can_open = background + ? (nfree > DWB_BG_RESERVE + DWB_EVICT_RESERVE || + (nfree >= 1 && nfree <= DWB_BG_RESERVE)) + : nfree > DWB_BG_RESERVE; open_idx = pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]); - if (nfree <= reserve && + if (!can_open && (open_idx == DWB_INVALID_BATCH || (pg_atomic_read_u32(&DWBCtl->batches[open_idx].next_slot_idx) & (DWB_SEAL_BIT | DWB_IDX_MASK)) >= (uint32) dwb_batch_pages)) From 5a7d7934919bcd1503527143492e8d11607722d4 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 1 Aug 2026 14:57:59 +0300 Subject: [PATCH 23/52] Check buffer permanence under the header lock in the checkpoint bin flush The bin gather trusted the caller's unlocked BM_PERMANENT pre-check, with only an Assert behind it: a buffer recycled for an unlogged page between the pre-check and the gather would have been staged into the double write buffer and its fake LSN fed to XLogFlush. Re-check under the buffer header lock and route non-permanent buffers to the per-page fallback, whose FlushBuffer skips both the WAL flush and the DWB for them. The deterministic unlogged paths were already pinned by t/002 (a shutdown checkpoint's unlogged buffers never enter the ring); the recycle window itself is instruction-scale and remains guard-only. --- src/backend/storage/buffer/bufmgr.c | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 01303f373b2bf..494b318703820 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -4070,8 +4070,13 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context) * claimed without waiting fall back to the ordinary per-page SyncOneBuffer * path after the bin is done, when nothing is held. * - * Caller guarantees every buffer is BM_PERMANENT (non-permanent checkpoint - * buffers take the per-page path). Returns the number of buffers written. + * The caller pre-filters for BM_PERMANENT, but only as an optimization: the + * authoritative check is made here under the buffer header lock, because a + * captured buffer can be recycled for an unlogged page before the bin + * flushes (the same benign window BufferSync already tolerates for the + * checkpoint-needed bit). Non-permanent buffers go to the per-page + * fallback, whose FlushBuffer skips both the WAL flush and the DWB for + * them. Returns the number of buffers written. */ static int FlushCkptBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) @@ -4113,7 +4118,18 @@ FlushCkptBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) UnlockBufHdr(bufHdr, buf_state); continue; } - Assert(buf_state & BM_PERMANENT); + if (!(buf_state & BM_PERMANENT)) + { + /* + * Recycled for an unlogged page after the bin captured it, or a + * shutdown checkpoint's unlogged buffer raced past the unlocked + * pre-check. Staging it would feed the DWB — and XLogFlush — + * a fake unlogged LSN, so route it to the per-page path instead. + */ + UnlockBufHdr(bufHdr, buf_state); + fb_ids[nfallback++] = buf_ids[i]; + continue; + } PinBuffer_Locked(bufHdr); if (!LWLockConditionalAcquire(BufferDescriptorGetContentLock(bufHdr), From ae03d4160ff9e0b819b88ae4a3dc7371d0c985e2 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 1 Aug 2026 19:30:24 +0300 Subject: [PATCH 24/52] Replace the ring-wait broadcast with targeted per-class wakeups (Stage 5) Under thousands of concurrent writers the single cv_free_batch condition variable collapsed: every batch SEAL pushed all same-class writers into DWBOpenNewBatch at once, each of them had to win one of the four staging buffers just to discover that somebody else had already opened the next batch, and every staging release or batch retirement broadcast to every waiter. A profile at 5000 connections showed ~74% of a 104-core machine spinning in s_lock on the condition variable's spinlock, 817K context switches per second, and the retire sink idle. Restructure the wait path: - cv_free_batch becomes cv_want_batch[writer class]: one queue per class, used by both the staging-pool and the ring-space waits (both conditions are re-checked in the common DWBOpenNewBatch loop). Splitting by class makes a wake-up impossible to lose across the class boundary, where the sliced reserves may forbid the woken class to open a batch. - DWBOpenNewBatch starts with a lock-free staleness check (the same SEAL-bit/class-bit test, authoritatively repeated under DWBRingOpenLock), so the herd that piles in after a SEAL returns to slot reservation without touching the staging pool or waking anyone. The staging reservation itself is now non-blocking; an empty pool is waited out in the same outer loop. - All wake-ups are targeted signals instead of broadcasts: a successful opener wakes up to batch_pages - 1 same-class waiters (the number of slots joiners can still take), and a staging release or a batch retirement wakes one would-be opener per class. The 1s sleep timeout stays as the lost-wakeup backstop, and the stall escalation clock is unchanged. The open-batch sharding idea (K open batches per class) stays shelved: the same profile shows the intra-batch rendezvous is not hot (DWBRingOpen waits ~0, coverage/fsync waits in the single digits out of 5000 waiters). Suite: test_dwb 15 files / 269 tests, core regress 231, concurrency tests looped 3x. --- src/backend/storage/dwb/dwb.c | 189 ++++++++++++++++++--------- src/backend/storage/dwb/dwb_ctl.c | 3 +- src/backend/storage/dwb/dwb_retire.c | 6 +- src/include/storage/dwb.h | 9 +- 4 files changed, 137 insertions(+), 70 deletions(-) diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 212691bbdccd8..8aa36e0408398 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -211,38 +211,24 @@ DWBWritesPaused(void) * staging pool * ---------------------------------------------------------------- */ +/* + * Non-blocking staging reservation: returns a buffer index, or -1 when the + * pool is empty. Waiting for a buffer happens in DWBOpenNewBatch's outer + * loop, together with the wait for ring space: a would-be opener needs both + * resources and re-checks both conditions on every wake-up. + */ static int -DWBStagingAlloc(void) +DWBStagingTryAlloc(void) { - int idx; - DWBStallState stall; + int idx = -1; - DWBStallInit(&stall); - for (;;) + SpinLockAcquire(&DWBCtl->staging_lock); + if (DWBCtl->staging_free != 0) { - idx = -1; - - SpinLockAcquire(&DWBCtl->staging_lock); - if (DWBCtl->staging_free != 0) - { - idx = pg_rightmost_one_pos32(DWBCtl->staging_free); - DWBCtl->staging_free &= ~(1U << idx); - } - SpinLockRelease(&DWBCtl->staging_lock); - - if (idx >= 0) - break; - - /* - * A buffer frees once its leader finishes the image pwrite; - * retirement broadcasts cv_free_batch too, so just re-check on every - * wake-up. The timeout only paces the stall clock. - */ - (void) ConditionVariableTimedSleep(&DWBCtl->cv_free_batch, 1000, - WAIT_EVENT_DWB_FREE_BATCH); - DWBStallCheck(&stall); + idx = pg_rightmost_one_pos32(DWBCtl->staging_free); + DWBCtl->staging_free &= ~(1U << idx); } - ConditionVariableCancelSleep(); + SpinLockRelease(&DWBCtl->staging_lock); return idx; } @@ -252,7 +238,43 @@ DWBStagingRelease(int idx) SpinLockAcquire(&DWBCtl->staging_lock); DWBCtl->staging_free |= 1U << idx; SpinLockRelease(&DWBCtl->staging_lock); - ConditionVariableBroadcast(&DWBCtl->cv_free_batch); + + /* the freed buffer admits one more opener */ + DWBWakeRingWaiters(); +} + +/* + * Wake one would-be batch opener of each writer class. Called whenever a + * resource an opener may be waiting for appears: a staging buffer returns to + * the pool or a batch returns to FREE. One targeted signal per class + * replaces a broadcast to every waiter, which collapses under thousands of + * ring-space waiters: each free event would wake them all just to re-queue + * on the condition variable's spinlock (3.6). Signalling per class rather + * than once overall is what makes a wake-up impossible to lose across the + * class boundary, where the sliced reserves (dwb.h) may forbid the woken + * class to open. A signal to an empty queue is a cheap no-op, every sleeper + * re-checks on a 1s timeout anyway, so over- and under-waking are both + * harmless. Allocation-free: legal inside critical sections. + */ +void +DWBWakeRingWaiters(void) +{ + for (int c = 0; c < DWB_NUM_WCLASSES; c++) + ConditionVariableSignal(&DWBCtl->cv_want_batch[c]); +} + +/* + * After opening a fresh batch, wake enough same-class waiters to fill it. + * The opener consumes one slot itself, so batch_pages - 1 joiners are the + * most that can make progress; the rest keep sleeping until the next open. + * The pipeline is self-clocking: the writer that overflows this batch seals + * it and opens the next one while already awake, waking the next portion. + */ +static void +DWBWakeJoiners(int wclass) +{ + for (int i = 0; i < dwb_batch_pages - 1; i++) + ConditionVariableSignal(&DWBCtl->cv_want_batch[wclass]); } /* ---------------------------------------------------------------- @@ -260,10 +282,43 @@ DWBStagingRelease(int idx) * ---------------------------------------------------------------- */ +/* + * Does open_batch_idx[wclass] still name the stale batch old_idx? Comparing + * the index alone is not enough: the ring reuses indexes, so by the time a + * slow opener asks, old_idx may name a NEW live incarnation of the same slot + * (sealed, retired, freed and reopened behind its back), and replacing it + * would orphan that live batch together with its staging buffer. SEAL_BIT + * plus the class bit disambiguate the incarnations: SEAL_BIT is set from + * SEAL through FREE and cleared only by the re-initialization in + * DWBOpenNewBatch (under DWBRingOpenLock), which also stamps the opening + * class — so the open batch needs replacing if and only if it is sealed or + * belongs to the other class (a reused index that the other class reopened + * while our pointer kept naming it). + * + * Callers outside DWBRingOpenLock use this as an opportunistic fast path; + * the opener re-checks under the lock before acting on the answer. + */ +static bool +DWBOpenBatchIsStale(int wclass, uint32 old_idx) +{ + uint32 cur = pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]); + uint32 nsi; + + if (cur != old_idx) + return false; /* someone already replaced it */ + if (cur == DWB_INVALID_BATCH) + return true; /* nothing open yet */ + + nsi = pg_atomic_read_u32(&DWBCtl->batches[cur].next_slot_idx); + return (nsi & DWB_SEAL_BIT) || + (nsi & DWB_WCLASS_BIT) != DWBWClassBit(wclass); +} + /* * Make open_batch_idx[wclass] point at an ALLOCATED batch, if it currently * points at old_idx (a sealed, foreign-class or invalid batch). Serialized - * by DWBRingOpenLock; sleeps on cv_free_batch when the whole ring is busy. + * by DWBRingOpenLock; sleeps on cv_want_batch[wclass] when the staging pool + * or the whole ring is busy. * * Ordering note for stale writers: a batch keeps SEAL_BIT in next_slot_idx * from its SEAL until we finish re-initializing it here, and the @@ -288,47 +343,47 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) int staging_idx; /* - * Reserve the staging buffer before taking the lock: the wait for a - * free buffer can be long, and no sleeping (or interruptible) point - * may exist below, where we hold DWBRingOpenLock with a batch already - * taken out of DWB_FREE. + * Fast path: if another opener already replaced the open batch, there + * is nothing left to do here. Checking this before the staging + * reservation matters under pressure: every SEAL pushes all + * concurrent same-class writers into this function at once, and all + * but one of them only need to learn the new index — sending them + * through the DWB_STAGING_BUFFERS-deep staging pool first would + * serialize the whole herd on it. The racy read is fine: whoever + * proceeds re-checks under DWBRingOpenLock below. */ - staging_idx = DWBStagingAlloc(); - - LWLockAcquire(DWBRingOpenLock, LW_EXCLUSIVE); + if (!DWBOpenBatchIsStale(wclass, old_idx)) + { + ConditionVariableCancelSleep(); + return; + } /* - * Someone else already replaced the open batch: done. Comparing the - * index alone is not enough: the ring reuses indexes, so by the time - * a slow opener gets here, old_idx may name a NEW live incarnation of - * the same slot (sealed, retired, freed and reopened behind our - * back), and replacing it would orphan that live batch together with - * its staging buffer. SEAL_BIT plus the class bit disambiguate the - * incarnations: SEAL_BIT is set from SEAL through FREE and cleared - * only by the re-initialization below (under this same lock), which - * also stamps the opening class — so the open batch needs replacing - * if and only if it is sealed or belongs to the other class (a reused - * index that the other class reopened while our pointer kept naming - * it). + * Try to become the opener. The staging buffer is reserved before + * taking the lock: no sleeping (or interruptible) point may exist + * below, where we hold DWBRingOpenLock with a batch already taken out + * of DWB_FREE. An empty pool is waited out in this outer loop: + * DWBStagingRelease signals cv_want_batch. */ + staging_idx = DWBStagingTryAlloc(); + if (staging_idx < 0) { - uint32 cur = pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]); - bool stale = (cur == old_idx); + (void) ConditionVariableTimedSleep(&DWBCtl->cv_want_batch[wclass], + 1000, + WAIT_EVENT_DWB_FREE_BATCH); + DWBStallCheck(&stall); + continue; + } - if (stale && cur != DWB_INVALID_BATCH) - { - uint32 nsi = pg_atomic_read_u32(&DWBCtl->batches[cur].next_slot_idx); + LWLockAcquire(DWBRingOpenLock, LW_EXCLUSIVE); - stale = (nsi & DWB_SEAL_BIT) || - (nsi & DWB_WCLASS_BIT) != DWBWClassBit(wclass); - } - if (!stale) - { - LWLockRelease(DWBRingOpenLock); - DWBStagingRelease(staging_idx); - ConditionVariableCancelSleep(); - return; - } + /* authoritative staleness re-check under the lock */ + if (!DWBOpenBatchIsStale(wclass, old_idx)) + { + LWLockRelease(DWBRingOpenLock); + DWBStagingRelease(staging_idx); + ConditionVariableCancelSleep(); + return; } /* @@ -397,6 +452,9 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) /* let retire workers re-time the force-seal deadline */ ConditionVariableBroadcast(&DWBCtl->cv_retire_wake); + + /* wake enough same-class waiters to fill the new batch */ + DWBWakeJoiners(wclass); return; } @@ -414,7 +472,8 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) if (DWBRetireAllSync() > 0) continue; - (void) ConditionVariableTimedSleep(&DWBCtl->cv_free_batch, 1000, + (void) ConditionVariableTimedSleep(&DWBCtl->cv_want_batch[wclass], + 1000, WAIT_EVENT_DWB_FREE_BATCH); DWBStallCheck(&stall); } @@ -489,7 +548,7 @@ DWBSealBatch(int batch_idx) batch->staging_idx = -1; pg_atomic_write_u32(&batch->state, DWB_FREE); pg_atomic_fetch_add_u64(&DWBCtl->freed_events, 1); - ConditionVariableBroadcast(&DWBCtl->cv_free_batch); + DWBWakeRingWaiters(); END_CRIT_SECTION(); return true; } diff --git a/src/backend/storage/dwb/dwb_ctl.c b/src/backend/storage/dwb/dwb_ctl.c index 1c68dfd6f6c6a..5324582155688 100644 --- a/src/backend/storage/dwb/dwb_ctl.c +++ b/src/backend/storage/dwb/dwb_ctl.c @@ -92,7 +92,8 @@ DWBShmemInit(void) pg_atomic_init_u32(&DWBCtl->open_batch_idx[i], DWB_INVALID_BATCH); pg_atomic_init_u64(&DWBCtl->next_batch_id, 1); pg_atomic_init_u64(&DWBCtl->freed_events, 0); - ConditionVariableInit(&DWBCtl->cv_free_batch); + for (int c = 0; c < DWB_NUM_WCLASSES; c++) + ConditionVariableInit(&DWBCtl->cv_want_batch[c]); ConditionVariableInit(&DWBCtl->cv_retire_wake); SpinLockInit(&DWBCtl->staging_lock); DWBCtl->staging_free = (1U << DWB_STAGING_BUFFERS) - 1; diff --git a/src/backend/storage/dwb/dwb_retire.c b/src/backend/storage/dwb/dwb_retire.c index e528988d048d6..3ced748adae4c 100644 --- a/src/backend/storage/dwb/dwb_retire.c +++ b/src/backend/storage/dwb/dwb_retire.c @@ -107,14 +107,14 @@ DWBFileTagFromSegRef(const DWSegRef *seg) */ /* - * Free a batch and wake everything that may be waiting for ring space. - * The caller has already moved the state to DWB_FREE. + * Free a batch and wake one would-be opener per writer class. The caller + * has already moved the state to DWB_FREE. */ static void DWBNoteBatchFreed(void) { pg_atomic_fetch_add_u64(&DWBCtl->freed_events, 1); - ConditionVariableBroadcast(&DWBCtl->cv_free_batch); + DWBWakeRingWaiters(); } /* diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 381c6b6cb47be..d513ecb462e19 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -346,7 +346,13 @@ typedef struct DWCtl pg_atomic_uint64 freed_events; /* monotonic count of batches that reached * FREE; backpressure waiters treat a * change as retire progress */ - ConditionVariable cv_free_batch; /* broadcast on retire */ + ConditionVariable cv_want_batch[DWB_NUM_WCLASSES]; /* per-class "want a + * batch" queue: both + * staging and + * ring-space waiters + * sleep here; woken by + * targeted signals, not + * broadcast (3.6) */ ConditionVariable cv_retire_wake; /* wakes retire workers */ slock_t staging_lock; /* protects staging_free bitmap */ uint32 staging_free; /* bitmap of free staging buffers */ @@ -391,6 +397,7 @@ extern DWBatchState DWBGetBatchState(int batch_idx); /* internal; exported for test_dwb's stale-open regression test */ extern void DWBOpenNewBatch(int wclass, uint32 old_idx); +extern void DWBWakeRingWaiters(void); /* dwb_retire.c — segment hash, retirement, worker pool */ struct FileTag; /* avoid dragging storage/sync.h in here */ From 5c3d74b32d1eee95906f26856b2fd908beaecc51 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 1 Aug 2026 19:42:20 +0300 Subject: [PATCH 25/52] Keep probe releases of staging buffers silent in the ring wait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With every DWBStagingRelease signalling cv_want_batch, a full ring turned the paced 1s waits into a busy rotation: a woken prober re-signalled the queue when it returned its unused staging buffer, handing the wake token to the next waiter (or straight back to itself — a timeout wake-up leaves the process queued on the condition variable, so its own signal can pop it), and the token circulated through DWBRingOpenLock acquisitions and ring scans indefinitely while nothing had changed. Make DWBStagingRelease silent and wake explicitly at the real capacity transitions only: a leader finishing its image pwrite (the buffer can serve the next batch), a batch returning to FREE, and a fresh batch opening. A probe-acquired buffer bouncing back unused wakes nobody; the 1s sleep timeout remains the backstop for the rare sleeper that raced against the last buffer. Add a shmem counter of wait-loop iterations that went to sleep (ring_wait_retries) with a test_dwb reader, and pin the pacing in the 003 real-clock scenario: a victim parked on an unchanged full ring for ~1s must accrue a handful of retries, not the thousands a rotating wake token produces. Suite: test_dwb 15 files / 271 tests, 003 looped 5x. --- src/backend/storage/dwb/dwb.c | 46 ++++++++++++------- src/backend/storage/dwb/dwb_ctl.c | 1 + src/include/storage/dwb.h | 7 +++ .../modules/test_dwb/t/003_backpressure.pl | 14 ++++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 4 ++ src/test/modules/test_dwb/test_dwb.c | 14 ++++++ 6 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 8aa36e0408398..cad8ccbf024f4 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -232,29 +232,39 @@ DWBStagingTryAlloc(void) return idx; } +/* + * Return a staging buffer to the pool. Deliberately silent: whether the + * return is a wake-worthy event depends on the caller. A leader finishing + * its image pwrite adds capacity and wakes (DWBWakeRingWaiters); a would-be + * opener returning a probe-acquired buffer it could not use must NOT wake — + * under a full ring the wake token would then circulate forever through the + * waiters (each woken prober re-signals on its own release, and a + * timeout-woken process is even still queued on the condition variable, so + * it can pop itself), turning the paced 1s waits into a busy rotation of + * DWBRingOpenLock acquisitions. + */ static void DWBStagingRelease(int idx) { SpinLockAcquire(&DWBCtl->staging_lock); DWBCtl->staging_free |= 1U << idx; SpinLockRelease(&DWBCtl->staging_lock); - - /* the freed buffer admits one more opener */ - DWBWakeRingWaiters(); } /* - * Wake one would-be batch opener of each writer class. Called whenever a - * resource an opener may be waiting for appears: a staging buffer returns to - * the pool or a batch returns to FREE. One targeted signal per class - * replaces a broadcast to every waiter, which collapses under thousands of - * ring-space waiters: each free event would wake them all just to re-queue - * on the condition variable's spinlock (3.6). Signalling per class rather - * than once overall is what makes a wake-up impossible to lose across the - * class boundary, where the sliced reserves (dwb.h) may forbid the woken - * class to open. A signal to an empty queue is a cheap no-op, every sleeper - * re-checks on a 1s timeout anyway, so over- and under-waking are both - * harmless. Allocation-free: legal inside critical sections. + * Wake one would-be batch opener of each writer class. Called on real + * capacity transitions only — a leader finished its image pwrite (the + * staging buffer serves the next batch) or a batch returned to FREE — never + * on a probe-acquired staging buffer bouncing back unused (see + * DWBStagingRelease). One targeted signal per class replaces a broadcast + * to every waiter, which collapses under thousands of ring-space waiters: + * each free event would wake them all just to re-queue on the condition + * variable's spinlock (3.6). Signalling per class rather than once overall + * is what makes a wake-up impossible to lose across the class boundary, + * where the sliced reserves (dwb.h) may forbid the woken class to open. A + * signal to an empty queue is a cheap no-op, every sleeper re-checks on a + * 1s timeout anyway, so over- and under-waking are both harmless. + * Allocation-free: legal inside critical sections. */ void DWBWakeRingWaiters(void) @@ -362,12 +372,14 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) * Try to become the opener. The staging buffer is reserved before * taking the lock: no sleeping (or interruptible) point may exist * below, where we hold DWBRingOpenLock with a batch already taken out - * of DWB_FREE. An empty pool is waited out in this outer loop: - * DWBStagingRelease signals cv_want_batch. + * of DWB_FREE. An empty pool is waited out in this outer loop: a + * leader wakes cv_want_batch when its image pwrite returns a buffer + * to the pool. */ staging_idx = DWBStagingTryAlloc(); if (staging_idx < 0) { + pg_atomic_fetch_add_u64(&DWBCtl->ring_wait_retries, 1); (void) ConditionVariableTimedSleep(&DWBCtl->cv_want_batch[wclass], 1000, WAIT_EVENT_DWB_FREE_BATCH); @@ -472,6 +484,7 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) if (DWBRetireAllSync() > 0) continue; + pg_atomic_fetch_add_u64(&DWBCtl->ring_wait_retries, 1); (void) ConditionVariableTimedSleep(&DWBCtl->cv_want_batch[wclass], 1000, WAIT_EVENT_DWB_FREE_BATCH); @@ -665,6 +678,7 @@ DWBLeaderWriteBatch(int batch_idx) /* image pwrite done — staging can serve the next batch */ DWBStagingRelease(batch->staging_idx); batch->staging_idx = -1; + DWBWakeRingWaiters(); expected = DWB_WRITTEN; if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, DWB_FSYNCED)) diff --git a/src/backend/storage/dwb/dwb_ctl.c b/src/backend/storage/dwb/dwb_ctl.c index 5324582155688..ec32d388b6186 100644 --- a/src/backend/storage/dwb/dwb_ctl.c +++ b/src/backend/storage/dwb/dwb_ctl.c @@ -92,6 +92,7 @@ DWBShmemInit(void) pg_atomic_init_u32(&DWBCtl->open_batch_idx[i], DWB_INVALID_BATCH); pg_atomic_init_u64(&DWBCtl->next_batch_id, 1); pg_atomic_init_u64(&DWBCtl->freed_events, 0); + pg_atomic_init_u64(&DWBCtl->ring_wait_retries, 0); for (int c = 0; c < DWB_NUM_WCLASSES; c++) ConditionVariableInit(&DWBCtl->cv_want_batch[c]); ConditionVariableInit(&DWBCtl->cv_retire_wake); diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index d513ecb462e19..c761c6e65df09 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -346,6 +346,13 @@ typedef struct DWCtl pg_atomic_uint64 freed_events; /* monotonic count of batches that reached * FREE; backpressure waiters treat a * change as retire progress */ + pg_atomic_uint64 ring_wait_retries; /* monotonic count of DWBOpenNewBatch + * iterations that went to sleep; a + * parked waiter on an unchanged ring + * must accrue these at the sleep + * timeout pace, not spin (see the + * silent-probe-release rule in + * DWBStagingRelease) */ ConditionVariable cv_want_batch[DWB_NUM_WCLASSES]; /* per-class "want a * batch" queue: both * staging and diff --git a/src/test/modules/test_dwb/t/003_backpressure.pl b/src/test/modules/test_dwb/t/003_backpressure.pl index f51a297d234a2..e0ed290d74406 100644 --- a/src/test/modules/test_dwb/t/003_backpressure.pl +++ b/src/test/modules/test_dwb/t/003_backpressure.pl @@ -148,8 +148,22 @@ $taken = $filler->query_safe('SELECT test_dwb_fill_ring()'); cmp_ok($taken, '>', 0, 'ring exhausted for the slow-warn scenario'); +my $retries0 = + $node->safe_psql('postgres', 'SELECT test_dwb_ring_wait_retries()'); + ($rc, $out, $err) = $node->psql('postgres', 'SELECT test_dwb_cycle(1)'); isnt($rc, 0, 'victim writer errors out on the real stall clock'); + +# Anti-spin regression: nothing woke the victim during its ~1s of waiting +# (no retire, no leader write), so its wait iterations must be paced by the +# 1s sleep timeout — a handful, not the thousands a self-waking rotation of +# the probe-released staging buffer would produce. +my $retries1 = + $node->safe_psql('postgres', 'SELECT test_dwb_ring_wait_retries()'); +cmp_ok($retries1 - $retries0, '>=', 1, + 'the stalled victim slept in the wait'); +cmp_ok($retries1 - $retries0, + '<=', 10, 'ring wait paced by the sleep timeout, not a busy rotation'); like( $err, qr/double write buffer has no free batch after/, diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index 465e34b494d68..ea3ea67b9adbc 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -23,6 +23,10 @@ CREATE FUNCTION test_dwb_states() RETURNS text STRICT AS 'MODULE_PATHNAME' LANGUAGE C; +CREATE FUNCTION test_dwb_ring_wait_retries() + RETURNS bigint STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + CREATE FUNCTION test_dwb_leak(npages int, do_publish bool) RETURNS void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index 30d0718c1f636..c616d13134765 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -338,6 +338,20 @@ test_dwb_ring_rel_slots(PG_FUNCTION_ARGS) PG_RETURN_INT32(count_ring_slots(false, true, relnumber)); } +/* + * Cumulative count of DWBOpenNewBatch iterations that went to sleep. The + * anti-spin regression in 003 asserts that a waiter parked on an unchanged + * full ring accrues these at the 1s sleep-timeout pace instead of busily + * rotating a wake token. + */ +PG_FUNCTION_INFO_V1(test_dwb_ring_wait_retries); +Datum +test_dwb_ring_wait_retries(PG_FUNCTION_ARGS) +{ + check_dwb_enabled(); + PG_RETURN_INT64((int64) pg_atomic_read_u64(&DWBCtl->ring_wait_retries)); +} + PG_FUNCTION_INFO_V1(test_dwb_states); Datum test_dwb_states(PG_FUNCTION_ARGS) From 477abf0ce320121d73da0532bff73720e849fa48 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 1 Aug 2026 20:55:14 +0300 Subject: [PATCH 26/52] Gate the self-help sweep and vectorize the bgwriter flush (Stage 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two sink-side costs measured on the 104-core stand after the wakeup-storm fix (2700 connections, 300s checkpoints): - ~51% of all CPU went to LWLock traffic under DWBRetireSweep: every writer parked on a full ring ran the self-help sweep before sleeping, and hundreds of concurrent sweepers hammered the per-batch publish locks and the segment hash (907 backends queued on DWBPublish) while losing almost every fsync claim to whoever got there first. Gate the self-help with a new DWBSelfSweep LWLock taken conditionally: one sweeper sweeps, trylock losers go straight to sleep and are woken by the winner's frees through cv_want_batch. An LWLock rather than an atomic flag keeps the gate error-safe (released by the unwind). - The bgwriter emitted 178.7K single-slot batches in 900s — one full batch fdatasync per scattered LRU page (199/s) and ~12% of the ring's batch turnover for ~0.4% of the pages. Route the LRU scan through the same bin machinery as the checkpoint flush: BgSyncPeekBuffer (the check half of SyncOneBuffer, same skip-recently-used semantics) classifies buffers, would-write candidates collect into a bin and flush as one batch via FlushBufferBin (renamed from FlushCkptBufferBin, with the bin cap now DWB_FLUSH_BIN_MAX), preserving the bgwriter_lru_maxpages budget and the reusable-buffers estimator. A buffer that changes between the peek and the flush is re-checked under the header lock inside the bin, exactly as for the checkpointer. New test t/016_bgwriter_bin.pl pins the bgwriter batching through pg_stat_io (average batch >= 4 slots under UPDATE passes through a small buffer pool). Suite: test_dwb 16 files / 272 tests, 016 looped 5x, core regress 231/231. --- src/backend/storage/buffer/bufmgr.c | 160 ++++++++++++++---- src/backend/storage/dwb/dwb.c | 27 ++- .../utils/activity/wait_event_names.txt | 1 + src/include/storage/lwlocklist.h | 1 + src/test/modules/test_dwb/meson.build | 1 + .../modules/test_dwb/t/016_bgwriter_bin.pl | 64 +++++++ 6 files changed, 210 insertions(+), 44 deletions(-) create mode 100644 src/test/modules/test_dwb/t/016_bgwriter_bin.pl diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 494b318703820..20603bed5f136 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -79,18 +79,21 @@ #define LocalBufHdrGetBlock(bufHdr) \ LocalBufferBlockPointers[-((bufHdr)->buf_id + 2)] -/* Bits in SyncOneBuffer's return value */ +/* Bits in SyncOneBuffer's (and BgSyncPeekBuffer's) return value */ #define BUF_WRITTEN 0x01 #define BUF_REUSABLE 0x02 +#define BUF_BINNABLE 0x04 /* would-write candidate for the + * vectored DWB flush bin */ /* - * Bin size cap for the vectored checkpoint flush (FlushCkptBufferBin): the - * flush holds a pin, a shared content lock and BM_IO_IN_PROGRESS per bin - * member at once, so the cap must leave MAX_SIMUL_LWLOCKS (200) plenty of + * Bin size cap for the vectored background flush (FlushBufferBin, used by + * the checkpointer's BufferSync and the bgwriter's LRU scan): the flush + * holds a pin, a shared content lock and BM_IO_IN_PROGRESS per bin member + * at once, so the cap must leave MAX_SIMUL_LWLOCKS (200) plenty of * headroom. 64 matches the default dwb_batch_pages; larger batch_pages * settings seal their batches at bin-sized fills. */ -#define CKPT_DWB_BIN_MAX 64 +#define DWB_FLUSH_BIN_MAX 64 #define RELS_BSEARCH_THRESHOLD 20 @@ -530,8 +533,9 @@ static void UnpinBuffer(BufferDesc *buf); static void UnpinBufferNoOwner(BufferDesc *buf); static void BufferSync(int flags); static uint32 WaitBufHdrUnlocked(BufferDesc *buf); -static int FlushCkptBufferBin(const int *buf_ids, int nbuf, - WritebackContext *wb_context); +static int FlushBufferBin(const int *buf_ids, int nbuf, + WritebackContext *wb_context); +static int BgSyncPeekBuffer(int buf_id); static int SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context); static void WaitIO(BufferDesc *buf); @@ -3447,11 +3451,11 @@ BufferSync(int flags) /* * With the double write buffer active, permanent buffers are flushed in * bins of up to a batch: one batch write and one fdatasync cover the - * whole bin instead of one per page (see FlushCkptBufferBin). + * whole bin instead of one per page (see FlushBufferBin). */ if (DWBIsEnabled() && !IsBootstrapProcessingMode()) { - dwb_bin_size = Min(dwb_batch_pages, CKPT_DWB_BIN_MAX); + dwb_bin_size = Min(dwb_batch_pages, DWB_FLUSH_BIN_MAX); dwb_bin = palloc(dwb_bin_size * sizeof(int)); } @@ -3600,8 +3604,8 @@ BufferSync(int flags) dwb_bin[dwb_bin_n++] = buf_id; if (dwb_bin_n == dwb_bin_size) { - int nw = FlushCkptBufferBin(dwb_bin, dwb_bin_n, - &wb_context); + int nw = FlushBufferBin(dwb_bin, dwb_bin_n, + &wb_context); PendingCheckpointerStats.buffers_written += nw; num_written += nw; @@ -3648,8 +3652,8 @@ BufferSync(int flags) { if (dwb_bin_n > 0) { - int nw = FlushCkptBufferBin(dwb_bin, dwb_bin_n, - &wb_context); + int nw = FlushBufferBin(dwb_bin, dwb_bin_n, + &wb_context); PendingCheckpointerStats.buffers_written += nw; num_written += nw; @@ -3727,6 +3731,11 @@ BgBufferSync(WritebackContext *wb_context) int num_written; int reusable_buffers; + /* Vectored DWB flush bin (bin_size stays 0 without the DWB) */ + int bin[DWB_FLUSH_BIN_MAX]; + int bin_n = 0; + int bin_size = 0; + /* Variables for final smoothed_density update */ long new_strategy_delta; uint32 new_recent_alloc; @@ -3907,11 +3916,38 @@ BgBufferSync(WritebackContext *wb_context) num_written = 0; reusable_buffers = reusable_buffers_est; + /* + * With the double write buffer active, would-write buffers are collected + * into bins and flushed as one batch each: one batch write and one + * fdatasync cover the whole bin instead of one per page (the LRU scan's + * scattered singleton writes otherwise degenerate to lone-writer batches; + * see FlushBufferBin). + */ + if (DWBIsEnabled()) + bin_size = Min(dwb_batch_pages, DWB_FLUSH_BIN_MAX); + /* Execute the LRU scan */ while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est) { - int sync_state = SyncOneBuffer(next_to_clean, true, - wb_context); + int sync_state; + + if (bin_size > 0) + sync_state = BgSyncPeekBuffer(next_to_clean); + else + sync_state = SyncOneBuffer(next_to_clean, true, wb_context); + + if (sync_state & BUF_BINNABLE) + { + bin[bin_n++] = next_to_clean; + reusable_buffers++; + } + else if (sync_state & BUF_WRITTEN) + { + reusable_buffers++; + num_written++; + } + else if (sync_state & BUF_REUSABLE) + reusable_buffers++; if (++next_to_clean >= NBuffers) { @@ -3920,19 +3956,29 @@ BgBufferSync(WritebackContext *wb_context) } num_to_scan--; - if (sync_state & BUF_WRITTEN) + /* + * Flush a full bin, and any partial one that already covers the + * remaining write budget: the cap check below must see the true + * written count, not a deferred bin. + */ + if (bin_n > 0 && + (bin_n == bin_size || + num_written + bin_n >= bgwriter_lru_maxpages)) { - reusable_buffers++; - if (++num_written >= bgwriter_lru_maxpages) - { - PendingBgWriterStats.maxwritten_clean++; - break; - } + num_written += FlushBufferBin(bin, bin_n, wb_context); + bin_n = 0; + } + + if (num_written >= bgwriter_lru_maxpages) + { + PendingBgWriterStats.maxwritten_clean++; + break; } - else if (sync_state & BUF_REUSABLE) - reusable_buffers++; } + if (bin_n > 0) + num_written += FlushBufferBin(bin, bin_n, wb_context); + PendingBgWriterStats.buf_written_clean += num_written; #ifdef BGW_DEBUG @@ -3971,6 +4017,45 @@ BgBufferSync(WritebackContext *wb_context) return (bufs_to_lap == 0 && recent_alloc == 0); } +/* + * BgSyncPeekBuffer -- the check half of SyncOneBuffer (with its + * skip_recently_used semantics) without the write. Classifies a buffer for + * the bgwriter's vectored flush: returns BUF_REUSABLE exactly as + * SyncOneBuffer would, plus BUF_BINNABLE when the buffer would have been + * written — the caller collects those into a bin and flushes them through + * the double write buffer as one batch (FlushBufferBin). The bin flush + * re-checks everything under the header lock, so a buffer that changes + * between the peek and the flush is handled there: clean again is skipped, + * recycled to unlogged goes to the per-page fallback, and a fresh pin or + * usage bump is the same benign race SyncOneBuffer itself has between its + * check and its write. + */ +static int +BgSyncPeekBuffer(int buf_id) +{ + BufferDesc *bufHdr = GetBufferDescriptor(buf_id); + int result = 0; + uint32 buf_state; + + buf_state = LockBufHdr(bufHdr); + + if (BUF_STATE_GET_REFCOUNT(buf_state) == 0 && + BUF_STATE_GET_USAGECOUNT(buf_state) == 0) + result |= BUF_REUSABLE; + else + { + /* recently used: not a replacement candidate, nothing to write */ + UnlockBufHdr(bufHdr, buf_state); + return result; + } + + if ((buf_state & BM_VALID) && (buf_state & BM_DIRTY)) + result |= BUF_BINNABLE; + + UnlockBufHdr(bufHdr, buf_state); + return result; +} + /* * SyncOneBuffer -- process a single buffer during syncing. * @@ -4052,16 +4137,17 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context) } /* - * FlushCkptBufferBin -- flush a bin of checkpoint buffers through the - * double write buffer as one batch. + * FlushBufferBin -- flush a bin of buffers through the double write buffer + * as one batch. Serves both background flushers: the checkpointer's + * BufferSync and the bgwriter's LRU scan. * * The per-page write protocol cannot amortize the batch fdatasync for a * sequential stream: each page waits for its own batch copy to become * durable before its data-file write, and the lone-writer seal then closes - * the batch over that single page — a checkpoint would pay one fdatasync - * per page. Here the whole bin is staged first, sealed and fdatasynced - * once, and only then written to the data files (the vectored background - * flush of the design, 3.4). + * the batch over that single page — a background flusher would pay one + * fdatasync per page. Here the whole bin is staged first, sealed and + * fdatasynced once, and only then written to the data files (the vectored + * background flush of the design, 3.4). * * All lock acquisitions in the gather phase are non-blocking: waiting for a * content lock or for somebody's buffer I/O while already holding shared @@ -4079,25 +4165,25 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context) * them. Returns the number of buffers written. */ static int -FlushCkptBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) +FlushBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) { static char *bin_buf = NULL; - BufferDesc *bufs[CKPT_DWB_BIN_MAX]; - XLogRecPtr lsns[CKPT_DWB_BIN_MAX]; - DWBSlotRef refs[CKPT_DWB_BIN_MAX]; - int fb_ids[CKPT_DWB_BIN_MAX]; + BufferDesc *bufs[DWB_FLUSH_BIN_MAX]; + XLogRecPtr lsns[DWB_FLUSH_BIN_MAX]; + DWBSlotRef refs[DWB_FLUSH_BIN_MAX]; + int fb_ids[DWB_FLUSH_BIN_MAX]; int gathered = 0; int nfallback = 0; int written = 0; XLogRecPtr max_lsn = InvalidXLogRecPtr; ErrorContextCallback errcallback; - Assert(nbuf > 0 && nbuf <= CKPT_DWB_BIN_MAX); + Assert(nbuf > 0 && nbuf <= DWB_FLUSH_BIN_MAX); if (bin_buf == NULL) bin_buf = MemoryContextAllocAligned(TopMemoryContext, - (Size) CKPT_DWB_BIN_MAX * BLCKSZ, + (Size) DWB_FLUSH_BIN_MAX * BLCKSZ, PG_IO_ALIGN_SIZE, 0); /* Phase 1: claim and copy what can be claimed without waiting */ diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index cad8ccbf024f4..a5c94f6324797 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -475,14 +475,27 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) /* * No usable FREE batch. Help ourselves before waiting: sweep the - * RETIRING batches synchronously. Under normal operation the worker - * pool keeps the ring ahead of the writers and this path is rare; - * when it does run, the per-segment claim keeps us and the workers - * from duplicating fsyncs. This is also what keeps the ring alive - * with dwb_retire_workers = 0 and in single-user mode. + * RETIRING batches synchronously. One sweeper at a time: with + * thousands of writers parked on a full ring, a sweep by every waiter + * is pure lock traffic — they hammer the per-batch publish locks + * and the segment hash while losing every fsync claim to whoever got + * there first (measured at ~half the CPU of a 104-core machine). A + * trylock loser skips straight to the sleep below and is woken + * through cv_want_batch by the winner's frees; the winner still + * shares the fsync work with the worker pool through the per-segment + * claims. The gate is an LWLock, not an atomic flag, so an ERROR + * inside the sweep releases it in the unwind. The self-help is also + * what keeps the ring alive with dwb_retire_workers = 0 and in + * single-user mode. */ - if (DWBRetireAllSync() > 0) - continue; + if (LWLockConditionalAcquire(DWBSelfSweepLock, LW_EXCLUSIVE)) + { + int swept = DWBRetireAllSync(); + + LWLockRelease(DWBSelfSweepLock); + if (swept > 0) + continue; + } pg_atomic_fetch_add_u64(&DWBCtl->ring_wait_retries, 1); (void) ConditionVariableTimedSleep(&DWBCtl->cv_want_batch[wclass], diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index c7f34ba36fb02..203a54dfbeff1 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -365,6 +365,7 @@ SerialControl "Waiting to read or update shared pg_serial s AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." DWBRingOpen "Waiting to open a new double write buffer batch." DWBSegHash "Waiting to read or update the double write buffer segment hash table." +DWBSelfSweep "Waiting to run the double write buffer self-help retirement sweep." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 3af3f4ad56a32..be378e51a0b34 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -86,3 +86,4 @@ PG_LWLOCK(52, SerialControl) PG_LWLOCK(53, AioWorkerSubmissionQueue) PG_LWLOCK(54, DWBRingOpen) PG_LWLOCK(55, DWBSegHash) +PG_LWLOCK(56, DWBSelfSweep) diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index fb0a591ac129e..36835f11a1905 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -52,6 +52,7 @@ tests += { 't/013_backup_start_point.pl', 't/014_pg_upgrade.pl', 't/015_vectored_flush.pl', + 't/016_bgwriter_bin.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/016_bgwriter_bin.pl b/src/test/modules/test_dwb/t/016_bgwriter_bin.pl new file mode 100644 index 0000000000000..71277e5daf307 --- /dev/null +++ b/src/test/modules/test_dwb/t/016_bgwriter_bin.pl @@ -0,0 +1,64 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# The bgwriter's LRU scan flushes through the double write buffer in bins +# (FlushBufferBin), like the checkpointer in 015: without the bins every +# scattered singleton write pays a full batch fdatasync through the +# lone-writer seal. pg_stat_io proves the batching: one dwb "write" is one +# batch, so write_bytes/writes is the average batch size. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_bgwriter'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 1 +autovacuum = off +shared_buffers = 2MB +bgwriter_delay = 10ms +bgwriter_lru_maxpages = 1000 +bgwriter_lru_multiplier = 10 +checkpoint_timeout = 1h +)); +$node->start; + +# A table several times larger than shared_buffers: every UPDATE pass +# streams allocations through the small pool, which is what makes the +# bgwriter clean dirty buffers ahead of the clock sweep. +$node->safe_psql( + 'postgres', q( + CREATE TABLE dwb_bgw (id int, pad text) WITH (fillfactor = 50); + INSERT INTO dwb_bgw SELECT g, repeat('b', 500) FROM generate_series(1, 20000) g; +)); + +# The bgwriter's write volume per round depends on its allocation estimator, +# so drive passes until its dwb statistics carry the proof; each pass is a +# fresh burst of allocations. writes >= 10 skips the noise of the first +# few partial bins. +my $binned = 0; +for my $pass (1 .. 8) +{ + $node->safe_psql('postgres', + "UPDATE dwb_bgw SET pad = repeat(chr(96 + $pass), 500)"); + $binned = $node->safe_psql( + 'postgres', q( + SELECT COALESCE(bool_or( + writes >= 10 + AND (write_bytes::numeric / writes - 4096) / 8192 >= 4), false) + FROM pg_stat_io + WHERE backend_type = 'background writer' AND object = 'dwb' + AND context = 'normal' + )); + last if $binned eq 't'; +} +is($binned, 't', 'bgwriter dwb batches average >= 4 slots, not one per page'); + +done_testing(); From 3745ed30b93d9f0e719036774ec80d925e66a372 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 1 Aug 2026 21:28:51 +0300 Subject: [PATCH 27/52] Pin the sweep gate with a test and tighten the bin comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups to the sweep-gate / bgwriter-bin commit: - New 003 scenario proves the single-sweeper property end to end: a dwb-self-sweep injection point sits inside the trylock-guarded section, so a process can only park there after winning DWBSelfSweepLock. With the ring exhausted and two stalled writers, the first victim parks at the point while the second one goes to sleep in the ring wait — the sleep proves it took the trylock-failed path, and pg_stat_activity shows exactly one process at the point. The point is detached before the wakeup: the winner re-enters the gate on its retry loop, and with the point still attached it would park again with no wakeup left to release it. The scenario then lets both victims finish: the ring is not required to go idle while they still write through it. - FlushBufferBin's header no longer claims every caller pre-filters BM_PERMANENT (the bgwriter's peek deliberately does not) and states explicitly that pin and usage counts are not re-checked past the callers' pick — the same benign race the per-page paths have. BgSyncPeekBuffer's comment lists exactly what the bin re-checks. - 016 resets shared io statistics after the data load so only the bgwriter's own writes enter the average, and polls between UPDATE passes instead of sampling once per pass. Suite: test_dwb 16 files / 275 tests, 003+016 looped 5x, pgindent and perltidy clean. --- src/backend/storage/buffer/bufmgr.c | 20 +++-- src/backend/storage/dwb/dwb.c | 6 +- .../modules/test_dwb/t/003_backpressure.pl | 86 ++++++++++++++++++- .../modules/test_dwb/t/016_bgwriter_bin.pl | 37 +++++--- 4 files changed, 125 insertions(+), 24 deletions(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 20603bed5f136..b718203b839b4 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -4024,11 +4024,13 @@ BgBufferSync(WritebackContext *wb_context) * SyncOneBuffer would, plus BUF_BINNABLE when the buffer would have been * written — the caller collects those into a bin and flushes them through * the double write buffer as one batch (FlushBufferBin). The bin flush - * re-checks everything under the header lock, so a buffer that changes - * between the peek and the flush is handled there: clean again is skipped, - * recycled to unlogged goes to the per-page fallback, and a fresh pin or - * usage bump is the same benign race SyncOneBuffer itself has between its - * check and its write. + * re-checks validity, dirtiness and permanence under the header lock, so a + * buffer that changes between the peek and the flush is handled there: + * clean again is skipped, recycled to unlogged goes to the per-page + * fallback. Pin and usage counts are not re-checked anywhere past this + * peek — a fresh pin or usage bump before the flush is the same benign + * race SyncOneBuffer itself has between its check and its write; + * skip_recently_used is an optimization, not a correctness contract. */ static int BgSyncPeekBuffer(int buf_id) @@ -4156,13 +4158,17 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context) * claimed without waiting fall back to the ordinary per-page SyncOneBuffer * path after the bin is done, when nothing is held. * - * The caller pre-filters for BM_PERMANENT, but only as an optimization: the + * A caller may pre-filter for BM_PERMANENT (BufferSync does, the bgwriter's + * peek deliberately does not), but that is only ever an optimization: the * authoritative check is made here under the buffer header lock, because a * captured buffer can be recycled for an unlogged page before the bin * flushes (the same benign window BufferSync already tolerates for the * checkpoint-needed bit). Non-permanent buffers go to the per-page * fallback, whose FlushBuffer skips both the WAL flush and the DWB for - * them. Returns the number of buffers written. + * them. Pin counts and usage counts are NOT re-checked here — writing a + * buffer that became recently-used after the caller picked it is the same + * benign race the per-page paths have between their check and their write. + * Returns the number of buffers written. */ static int FlushBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index a5c94f6324797..31bd54bdd9db7 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -490,8 +490,12 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) */ if (LWLockConditionalAcquire(DWBSelfSweepLock, LW_EXCLUSIVE)) { - int swept = DWBRetireAllSync(); + int swept; + /* test hook: proves at most one waiter ever gets here at a time */ + INJECTION_POINT("dwb-self-sweep", NULL); + + swept = DWBRetireAllSync(); LWLockRelease(DWBSelfSweepLock); if (swept > 0) continue; diff --git a/src/test/modules/test_dwb/t/003_backpressure.pl b/src/test/modules/test_dwb/t/003_backpressure.pl index e0ed290d74406..918d243980a56 100644 --- a/src/test/modules/test_dwb/t/003_backpressure.pl +++ b/src/test/modules/test_dwb/t/003_backpressure.pl @@ -53,6 +53,88 @@ FROM generate_series(1, 1000) g; )); +# --- the self-help sweep admits exactly one sweeper ----------------------- + +# The dwb-self-sweep point sits INSIDE the trylock-guarded section, so a +# process can only park there after winning DWBSelfSweepLock. With the +# ring exhausted and two writers stalled, exactly one may hold the gate: +# the loser must be asleep in the ring wait, not sweeping. +$node->safe_psql('postgres', + "SELECT injection_points_attach('dwb-self-sweep', 'wait')"); + +my $filler = $node->background_psql('postgres'); +my $taken = $filler->query_safe('SELECT test_dwb_fill_ring()'); +cmp_ok($taken, '>', 0, 'ring exhausted for the sweep-gate scenario'); + +# Two victims, each forced to evict dirty pages through the full ring. +my $victim1 = $node->background_psql('postgres'); +$victim1->query_until( + qr/starting_victim1/, q( +\echo starting_victim1 +CREATE TABLE dwb_sweep_v1 AS + SELECT g AS id, repeat('1', 300) AS filler + FROM generate_series(1, 80000) g; +)); +$node->wait_for_event('client backend', 'dwb-self-sweep'); + +my $victim2 = $node->background_psql('postgres'); +$victim2->query_until( + qr/starting_victim2/, q( +\echo starting_victim2 +CREATE TABLE dwb_sweep_v2 AS + SELECT g AS id, repeat('2', 300) AS filler + FROM generate_series(1, 80000) g; +)); + +# The loser's sleep in the ring wait proves it took the trylock-failed +# path: the winner's path parks at the injection point before any sleep. +$node->wait_for_event('client backend', 'DwbFreeBatch'); +is( $node->safe_psql( + 'postgres', + "SELECT count(*) FROM pg_stat_activity WHERE wait_event = 'dwb-self-sweep'" + ), + '1', + 'exactly one process is inside the self-help sweep gate'); + +# Detach BEFORE waking: the winner re-enters the gate on its retry loop, +# and with the point still attached it would park again with no wakeup +# left to release it. +$node->safe_psql('postgres', + "SELECT injection_points_detach('dwb-self-sweep')"); +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('dwb-self-sweep')"); + +# Release the filler's leaked refs: the victims then finish on their own +# (no worker pool, so they seal and retire synchronously as they write). +# The abandoned batches need a seal/retire nudge from the poll, but the +# ring must NOT be required to go idle here — the victims keep batches in +# flight until they commit, so wait for their tables first. pg_class only +# shows them once the statements committed. +$filler->quit; +$node->poll_query_until('postgres', + "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " + . "CASE WHEN test_dwb_retire() >= 0 THEN " + . "count(*) = 2 END END FROM pg_class " + . "WHERE relname IN ('dwb_sweep_v1', 'dwb_sweep_v2')") + or die 'timed out waiting for the sweep-gate victims to finish'; +is( $node->safe_psql( + 'postgres', + 'SELECT (SELECT count(*) FROM dwb_sweep_v1) + (SELECT count(*) FROM dwb_sweep_v2)' + ), + '160000', + 'both stalled victims completed after the ring drained'); +$victim1->quit; +$victim2->quit; +$node->safe_psql('postgres', 'DROP TABLE dwb_sweep_v1, dwb_sweep_v2'); + +# With the victims gone the ring drains to idle for the next scenario. +$node->poll_query_until('postgres', + "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " + . "CASE WHEN test_dwb_retire() >= 0 THEN " + . "test_dwb_states() LIKE 'free=16 %' END END") + or die + 'timed out waiting for the ring to drain after the sweep-gate scenario'; + # --- ERROR in a non-critical writer keeps the cluster alive -------------- # Attach while the ring is still healthy; the point only fires for a @@ -60,8 +142,8 @@ $node->safe_psql('postgres', "SELECT injection_points_attach('dwb-force-stall', 'notice')"); -my $filler = $node->background_psql('postgres'); -my $taken = $filler->query_safe('SELECT test_dwb_fill_ring()'); +$filler = $node->background_psql('postgres'); +$taken = $filler->query_safe('SELECT test_dwb_fill_ring()'); cmp_ok($taken, '>', 0, 'ring exhausted by leaked refs'); # The victim outgrows shared_buffers, so it must evict its own dirty pages diff --git a/src/test/modules/test_dwb/t/016_bgwriter_bin.pl b/src/test/modules/test_dwb/t/016_bgwriter_bin.pl index 71277e5daf307..7118b69cbfebe 100644 --- a/src/test/modules/test_dwb/t/016_bgwriter_bin.pl +++ b/src/test/modules/test_dwb/t/016_bgwriter_bin.pl @@ -12,6 +12,7 @@ use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; +use Time::HiRes qw(usleep); my $node = PostgreSQL::Test::Cluster->new('dwb_bgwriter'); $node->init; @@ -39,25 +40,33 @@ INSERT INTO dwb_bgw SELECT g, repeat('b', 500) FROM generate_series(1, 20000) g; )); +# Only the bgwriter's own writes may enter the average: drop what the +# initial data load accumulated. +$node->safe_psql('postgres', "SELECT pg_stat_reset_shared('io')"); + # The bgwriter's write volume per round depends on its allocation estimator, -# so drive passes until its dwb statistics carry the proof; each pass is a -# fresh burst of allocations. writes >= 10 skips the noise of the first -# few partial bins. +# and its statistics reach the collector asynchronously: drive passes of +# fresh allocations and poll between them until the dwb row carries the +# proof. writes >= 10 skips the noise of the first few partial bins. my $binned = 0; -for my $pass (1 .. 8) +OUTER: for my $pass (1 .. 8) { $node->safe_psql('postgres', "UPDATE dwb_bgw SET pad = repeat(chr(96 + $pass), 500)"); - $binned = $node->safe_psql( - 'postgres', q( - SELECT COALESCE(bool_or( - writes >= 10 - AND (write_bytes::numeric / writes - 4096) / 8192 >= 4), false) - FROM pg_stat_io - WHERE backend_type = 'background writer' AND object = 'dwb' - AND context = 'normal' - )); - last if $binned eq 't'; + for my $probe (1 .. 25) + { + $binned = $node->safe_psql( + 'postgres', q( + SELECT COALESCE(bool_or( + writes >= 10 + AND (write_bytes::numeric / writes - 4096) / 8192 >= 4), false) + FROM pg_stat_io + WHERE backend_type = 'background writer' AND object = 'dwb' + AND context = 'normal' + )); + last OUTER if $binned eq 't'; + usleep(200_000); + } } is($binned, 't', 'bgwriter dwb batches average >= 4 slots, not one per page'); From 6c7a6d9fde267a3e1e9668a26a2c22dd1c6aab6e Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sun, 2 Aug 2026 13:01:20 +0300 Subject: [PATCH 28/52] Retire batches with syncfs and account seal reasons (Stage 5) A shared random workload touches nearly every segment of a large table between retire rounds, so the per-segment protocol pays hundreds of fdatasync calls per sweep and a batch waits for the sweep to visit all of its segments before it can be freed. The new dwb_retire_sync_method (default syncfs where available) makes a retire round issue one syncfs() per file system holding data files and then free every batch that was already RETIRING when the round began: their data-file writes preceded the syncfs, so they are durable. The per-segment path stays as the fallback and keeps serving the checkpointer piggyback and the OOM retire in both modes. Also add per-class, per-reason seal counters (overflow, lone-writer, waiter timeout, worker timeout, bin flush, forced) with slot sums, exposed through test_dwb_seal_stats(): client batches average half-full on the perf stand and the counters attribute that turnover to its trigger. New TAP test 017 drives a workload through a worker running syncfs rounds with fsync on and crash-checks the result; 002 asserts the no-pool lone-seal accounting, 004 pins the fsync method to keep the per-segment sweep covered, and the regress test pins the deterministic overflow/forced counts. --- doc/src/sgml/config.sgml | 29 +++ src/backend/storage/dwb/dwb.c | 26 ++- src/backend/storage/dwb/dwb_ctl.c | 7 + src/backend/storage/dwb/dwb_retire.c | 208 +++++++++++++++++- .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/misc/guc_tables.c | 11 + src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/storage/dwb.h | 41 +++- .../modules/test_dwb/expected/test_dwb.out | 10 + src/test/modules/test_dwb/meson.build | 1 + src/test/modules/test_dwb/sql/test_dwb.sql | 5 + .../modules/test_dwb/t/002_flushbuffer.pl | 10 + .../modules/test_dwb/t/004_retire_paths.pl | 1 + .../modules/test_dwb/t/017_syncfs_retire.pl | 74 +++++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 5 + src/test/modules/test_dwb/test_dwb.c | 44 +++- src/tools/pgindent/typedefs.list | 1 + 17 files changed, 455 insertions(+), 21 deletions(-) create mode 100644 src/test/modules/test_dwb/t/017_syncfs_retire.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index ec446895bacc0..1e38f1ba15b92 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3554,6 +3554,35 @@ include_dir 'conf.d' + + dwb_retire_sync_method (enum) + + dwb_retire_sync_method configuration parameter + + + + + Selects how retirement makes the covered data files durable before + freeing a batch. With fsync each touched + data-file segment is synced individually. With + syncfs a retire round issues one + syncfs() call per file system holding data + files and then frees every batch whose data-file writes preceded + the round, which is much cheaper when a write-heavy workload + touches many segments between rounds. syncfs + is only available on Linux and is the default there; + fsync is the default elsewhere. + This parameter can only be set in the postgresql.conf + file or on the server command line. + + + The caveats of syncfs described under + apply here as + well. + + + + dwb_batch_timeout_ms (integer) diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 31bd54bdd9db7..6aa64bdc2b327 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -84,7 +84,7 @@ typedef struct DWBStallState static void DWBProcExit(int code, Datum arg); static void DWBLeaderWriteBatch(int batch_idx); -static bool DWBSealBatch(int batch_idx); +static bool DWBSealBatch(int batch_idx, DWBSealReason reason); static void DWBFinishBatchData(DWBatchCtl *batch); static void DWBAbandonRef(DWBPendingRef *pref); static void ResOwnerReleaseDWBRef(Datum res); @@ -520,12 +520,13 @@ DWBOpenNewBatch(int wclass, uint32 old_idx) * the defensive capped_slots == 0 case — on return). */ static bool -DWBSealBatch(int batch_idx) +DWBSealBatch(int batch_idx, DWBSealReason reason) { DWBatchCtl *batch = &DWBCtl->batches[batch_idx]; uint32 prev; uint32 capped; uint32 expected; + int wclass; /* * Get everything the critical section below could fail at out of the way @@ -562,6 +563,11 @@ DWBSealBatch(int batch_idx) pg_atomic_write_u32(&batch->capped_slots, capped); pg_write_barrier(); + /* diagnostic accounting: who seals, and how full the batches are */ + wclass = (prev & DWB_WCLASS_BIT) ? DWB_WCLASS_BACKGROUND : DWB_WCLASS_EVICTION; + pg_atomic_fetch_add_u64(&DWBCtl->seal_count[wclass][reason], 1); + pg_atomic_fetch_add_u64(&DWBCtl->seal_pages[wclass][reason], capped); + expected = DWB_ALLOCATED; if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, DWB_SEALED)) elog(PANIC, "DWB batch %d sealed in unexpected state %u", @@ -721,7 +727,7 @@ DWBLeaderWriteBatch(int batch_idx) * and non-empty. */ bool -DWBTrySealBatch(int batch_idx) +DWBTrySealBatch(int batch_idx, DWBSealReason reason) { DWBatchCtl *batch = &DWBCtl->batches[batch_idx]; uint32 nsi = pg_atomic_read_u32(&batch->next_slot_idx); @@ -733,7 +739,7 @@ DWBTrySealBatch(int batch_idx) return false; /* empty: sealing buys nothing */ if (pg_atomic_read_u32(&batch->state) != DWB_ALLOCATED) return false; - return DWBSealBatch(batch_idx); + return DWBSealBatch(batch_idx, reason); } /* @@ -827,7 +833,7 @@ DWBAcquireSlot(const BufferTag *tag, int wclass, bool use_resowner, if (slot >= (uint32) dwb_batch_pages) { /* overflow: this writer seals and (if it wins) leads */ - DWBSealBatch(idx); + DWBSealBatch(idx, DWB_SEAL_OVERFLOW); DWBOpenNewBatch(wclass, idx); continue; } @@ -932,7 +938,7 @@ DWBWaitBatchFsynced(const DWBSlotRef *ref) * the next batch — sealing is valid at any moment. */ if (pg_atomic_read_u32(&batch->ref_count) == 1) - (void) DWBTrySealBatch(ref->batch_idx); + (void) DWBTrySealBatch(ref->batch_idx, DWB_SEAL_LONE); ConditionVariablePrepareToSleep(&batch->cv_state); while (pg_atomic_read_u32(&batch->state) < DWB_FSYNCED) @@ -940,7 +946,7 @@ DWBWaitBatchFsynced(const DWBSlotRef *ref) if (ConditionVariableTimedSleep(&batch->cv_state, dwb_batch_timeout_ms, WAIT_EVENT_DWB_BATCH_FSYNC)) - (void) DWBTrySealBatch(ref->batch_idx); + (void) DWBTrySealBatch(ref->batch_idx, DWB_SEAL_WAIT_TIMEOUT); } ConditionVariableCancelSleep(); } @@ -1005,7 +1011,7 @@ DWBForceSealOpenBatch(int wclass) if (idx == DWB_INVALID_BATCH) return false; - return DWBTrySealBatch((int) idx); + return DWBTrySealBatch((int) idx, DWB_SEAL_FORCED); } DWBatchState @@ -1053,7 +1059,7 @@ DWBStagePageWrite(const BufferTag *tag, const char *image, * instead of paying dwb_batch_timeout_ms per page. */ if (dwb_retire_workers == 0 || !IsUnderPostmaster) - (void) DWBTrySealBatch(ref->batch_idx); + (void) DWBTrySealBatch(ref->batch_idx, DWB_SEAL_LONE); DWBWaitBatchFsynced(ref); @@ -1092,7 +1098,7 @@ DWBWaitStagedWrites(const DWBSlotRef *refs, int nrefs) { for (int i = 0; i < nrefs; i++) if (i == 0 || refs[i].batch_idx != refs[i - 1].batch_idx) - (void) DWBTrySealBatch(refs[i].batch_idx); + (void) DWBTrySealBatch(refs[i].batch_idx, DWB_SEAL_BIN); for (int i = 0; i < nrefs; i++) if (i == 0 || refs[i].batch_idx != refs[i - 1].batch_idx) diff --git a/src/backend/storage/dwb/dwb_ctl.c b/src/backend/storage/dwb/dwb_ctl.c index ec32d388b6186..b132039f448b2 100644 --- a/src/backend/storage/dwb/dwb_ctl.c +++ b/src/backend/storage/dwb/dwb_ctl.c @@ -25,6 +25,7 @@ int dwb_num_batches = 64; int dwb_batch_pages = 64; int dwb_max_segments = 4096; int dwb_retire_workers = 1; +int dwb_retire_sync_method = DWB_RETIRE_SYNC_METHOD_DEFAULT; int dwb_batch_timeout_ms = 10; int dwb_retire_interval_ms = 50; bool dwb_writeback = true; @@ -93,6 +94,12 @@ DWBShmemInit(void) pg_atomic_init_u64(&DWBCtl->next_batch_id, 1); pg_atomic_init_u64(&DWBCtl->freed_events, 0); pg_atomic_init_u64(&DWBCtl->ring_wait_retries, 0); + for (int c = 0; c < DWB_NUM_WCLASSES; c++) + for (int r = 0; r < DWB_SEAL_NREASONS; r++) + { + pg_atomic_init_u64(&DWBCtl->seal_count[c][r], 0); + pg_atomic_init_u64(&DWBCtl->seal_pages[c][r], 0); + } for (int c = 0; c < DWB_NUM_WCLASSES; c++) ConditionVariableInit(&DWBCtl->cv_want_batch[c]); ConditionVariableInit(&DWBCtl->cv_retire_wake); diff --git a/src/backend/storage/dwb/dwb_retire.c b/src/backend/storage/dwb/dwb_retire.c index 3ced748adae4c..5b96d09df5400 100644 --- a/src/backend/storage/dwb/dwb_retire.c +++ b/src/backend/storage/dwb/dwb_retire.c @@ -28,6 +28,12 @@ * All of them share this accounting; duplicate fsyncs are wasted work at * worst, never a correctness problem. * + * With dwb_retire_sync_method = syncfs the worker pool and the self-help + * skip the per-segment protocol entirely: one syncfs() round makes every + * file system holding data files durable and frees all batches that were + * RETIRING when the round began (DWBRetireRoundSyncfs). The checkpointer + * piggyback keeps using the per-segment accounting in both modes. + * * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * @@ -38,8 +44,12 @@ */ #include "postgres.h" +#include +#include + #include "common/hashfn.h" #include "common/int.h" +#include "common/relpath.h" #include "miscadmin.h" #include "port/pg_bitutils.h" #include "postmaster/bgworker.h" @@ -56,6 +66,7 @@ static void DWBMaybeRemoveSegEntry(DWSegEntry *entry); static int DWBRetireSweep(int worker_id); +static int DWBRetireRoundSyncfs(void); /* * Snapshot of one segment's back-references, taken before an fsync of that @@ -531,16 +542,16 @@ DWBRetireSegment(const DWSegRef *seg) } /* - * One retire sweep over all RETIRING batches, oldest first. worker_id >= 0 - * restricts the sweep to that worker's segment partition; -1 sweeps - * everything: the self-help of a writer stuck on a full ring - * (DWBOpenNewBatch) and the synchronous retire in DWBFinishPageWrite when - * there is no worker pool (dwb_retire_workers = 0, single-user mode). - * Returns batches freed. + * Retire everything that can be retired right now. Called by the self-help + * of a writer stuck on a full ring (DWBOpenNewBatch) and by the synchronous + * retire in DWBFinishPageWrite when there is no worker pool + * (dwb_retire_workers = 0, single-user mode). Returns batches freed. */ int DWBRetireAllSync(void) { + if (dwb_retire_sync_method == DATA_DIR_SYNC_METHOD_SYNCFS) + return DWBRetireRoundSyncfs(); return DWBRetireSweep(-1); } @@ -557,6 +568,11 @@ dwb_retiring_batch_cmp(const void *a, const void *b) ((const DWBRetiringBatch *) b)->id); } +/* + * One per-segment retire sweep over all RETIRING batches, oldest first. + * worker_id >= 0 restricts the sweep to that worker's segment partition; + * -1 sweeps everything. Returns batches freed. + */ static int DWBRetireSweep(int worker_id) { @@ -603,6 +619,179 @@ DWBRetireSweep(int worker_id) return freed; } +#ifdef HAVE_SYNCFS +/* + * syncfs() one directory's file system. Follows the vanilla data_sync_retry + * policy of DWBRetireSyncSegment: a failure PANICs by default, or WARNs and + * returns false under data_sync_retry = on so the caller retries the round + * later. (The usual retry caveat applies doubly here: a second syncfs may + * report success after the kernel already dropped the dirty pages the first + * failure was about.) missing_ok tolerates a dangling pg_tblspc entry: a + * vanished tablespace took its data files with it, so their writes are as + * moot as a dropped segment's on the per-segment path. + */ +static bool +DWBSyncfsPath(const char *path, bool missing_ok) +{ + int fd; + + fd = OpenTransientFile(path, O_RDONLY); + if (fd < 0) + { + if (missing_ok && errno == ENOENT) + return true; + ereport(data_sync_elevel(WARNING), + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", path))); + return false; + } + pgstat_report_wait_start(WAIT_EVENT_DWB_SYNCFS); + if (syncfs(fd) < 0) + { + pgstat_report_wait_end(); + CloseTransientFile(fd); + ereport(data_sync_elevel(WARNING), + (errcode_for_file_access(), + errmsg("could not synchronize file system for file \"%s\": %m", + path))); + return false; + } + pgstat_report_wait_end(); + CloseTransientFile(fd); + return true; +} +#endif /* HAVE_SYNCFS */ + +/* + * Make every file system that can hold data files durable: the one under + * the data directory (the process is chdir'd into it) and each tablespace + * mount. Returns true only if every syncfs succeeded — anything less and + * no batch may be freed on its account. An unreadable pg_tblspc raises an + * ERROR (never a wrong free): the worker restarts, a self-helping writer + * aborts its statement. + */ +static bool +DWBSyncfsAllFilesystems(void) +{ +#ifdef HAVE_SYNCFS + DIR *dir; + struct dirent *de; + bool ok = true; + + if (!enableFsync) + return true; + + if (!DWBSyncfsPath(".", false)) + ok = false; + + dir = AllocateDir(PG_TBLSPC_DIR); + while ((de = ReadDir(dir, PG_TBLSPC_DIR)) != NULL) + { + char path[MAXPGPATH]; + + if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + snprintf(path, MAXPGPATH, "%s/%s", PG_TBLSPC_DIR, de->d_name); + if (!DWBSyncfsPath(path, true)) + ok = false; + } + FreeDir(dir); + return ok; +#else + /* the GUC cannot be set to syncfs without HAVE_SYNCFS */ + elog(PANIC, "syncfs is not supported on this platform"); + return false; /* keep the compiler happy */ +#endif +} + +/* + * One wholesale retire round: syncfs the file systems and free every batch + * that was already RETIRING when the round began. Returns batches freed. + * + * Correctness of the wholesale free: a batch observed RETIRING completed + * ALL its data-file writes before the last ref drop published it (write + * path steps 6-7), so those writes were submitted before syncfs() started + * and are durable when it returns. A batch that reaches RETIRING while + * the syncfs runs is not on the list and waits for the next round. + * + * The per-segment accounting stays consistent with the concurrent + * checkpointer piggyback: each freed batch's bits are cleared under the + * same publish_lock + DWBSegHashLock the decrement path takes, and the + * (batch_idx, batch_id) snapshot re-check under publish_lock is the same + * ABA guard the snapshot protocol uses (batch_id is read racily here, like + * in DWBRetireSweep's collect; a torn read only makes the re-check skip). + */ +static int +DWBRetireRoundSyncfs(void) +{ + DWBRetiringBatch *retiring; + int nretiring = 0; + int freed = 0; + + retiring = palloc(dwb_num_batches * sizeof(DWBRetiringBatch)); + + for (int i = 0; i < dwb_num_batches; i++) + { + if (pg_atomic_read_u32(&DWBCtl->batches[i].state) == DWB_RETIRING) + { + retiring[nretiring].idx = i; + retiring[nretiring].id = DWBCtl->batches[i].batch_id; + nretiring++; + } + } + + if (nretiring == 0 || !DWBSyncfsAllFilesystems()) + { + pfree(retiring); + return 0; + } + + for (int i = 0; i < nretiring; i++) + { + int idx = retiring[i].idx; + DWBatchCtl *batch = &DWBCtl->batches[idx]; + + LWLockAcquire(&batch->publish_lock, LW_EXCLUSIVE); + if (batch->batch_id == retiring[i].id && + pg_atomic_read_u32(&batch->state) == DWB_RETIRING) + { + uint32 expected = DWB_RETIRING; + uint64 bit = UINT64CONST(1) << (idx % 64); + + LWLockAcquire(DWBSegHashLock, LW_EXCLUSIVE); + for (uint32 s = 0; s < batch->n_segs; s++) + { + DWSegEntry *entry; + + entry = (DWSegEntry *) hash_search(DWSegmentHash, + &batch->seg_set[s], + HASH_FIND, NULL); + if (entry != NULL) + { + uint64 prev; + + prev = pg_atomic_fetch_and_u64(&entry->batch_bitmap[idx / 64], + ~bit); + if (prev & bit) + DWBMaybeRemoveSegEntry(entry); + } + } + LWLockRelease(DWBSegHashLock); + + pg_atomic_write_u32(&batch->seg_pending_count, 0); + if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, + DWB_FREE)) + elog(PANIC, "DWB batch freed in unexpected state %u", expected); + DWBNoteBatchFreed(); + freed++; + } + LWLockRelease(&batch->publish_lock); + } + + pfree(retiring); + return freed; +} + /* ---------------------------------------------------------------- * retire worker pool * ---------------------------------------------------------------- @@ -715,12 +904,15 @@ DWBRetireWorkerMain(Datum main_arg) age_ms = TimestampDifferenceMilliseconds(batch->open_time, now); if (age_ms >= dwb_batch_timeout_ms) - (void) DWBTrySealBatch(i); + (void) DWBTrySealBatch(i, DWB_SEAL_WORKER_TIMEOUT); else if (dwb_batch_timeout_ms - age_ms < timeout) timeout = dwb_batch_timeout_ms - age_ms; } - (void) DWBRetireSweep(my_id); + if (dwb_retire_sync_method == DATA_DIR_SYNC_METHOD_SYNCFS) + (void) DWBRetireRoundSyncfs(); + else + (void) DWBRetireSweep(my_id); (void) ConditionVariableTimedSleep(&DWBCtl->cv_retire_wake, Max(timeout, 1), diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 203a54dfbeff1..4fb0658545642 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -230,6 +230,7 @@ DWB_CONTROL_READ "Waiting for a read of the double write buffer control file." DWB_CONTROL_SYNC "Waiting for the double write buffer control file to reach durable storage." DWB_CONTROL_WRITE "Waiting for a write to the double write buffer control file." DWB_RING_INIT "Waiting for preallocation of the double write buffer ring files." +DWB_SYNCFS "Waiting for a syncfs() call that retires double write buffer batches." LOCK_FILE_ADDTODATADIR_READ "Waiting for a read while adding a line to the data directory lock file." LOCK_FILE_ADDTODATADIR_SYNC "Waiting for data to reach durable storage while adding a line to the data directory lock file." LOCK_FILE_ADDTODATADIR_WRITE "Waiting for a write while adding a line to the data directory lock file." diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 5a145be30df8f..cc5b410b8e1a2 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -452,6 +452,7 @@ static const struct config_enum_entry debug_logical_replication_streaming_option StaticAssertDecl(lengthof(ssl_protocol_versions_info) == (PG_TLS1_3_VERSION + 2), "array length mismatch"); +/* shared by recovery_init_sync_method and dwb_retire_sync_method */ static const struct config_enum_entry recovery_init_sync_method_options[] = { {"fsync", DATA_DIR_SYNC_METHOD_FSYNC, false}, #ifdef HAVE_SYNCFS @@ -5138,6 +5139,16 @@ struct config_enum ConfigureNamesEnum[] = NULL, NULL, NULL }, + { + {"dwb_retire_sync_method", PGC_SIGHUP, WAL_SETTINGS, + gettext_noop("Selects how double write buffer retirement makes data files durable."), + gettext_noop("\"fsync\" syncs the touched data-file segments one by one; \"syncfs\" syncs their whole file systems per retire round.") + }, + &dwb_retire_sync_method, + DWB_RETIRE_SYNC_METHOD_DEFAULT, recovery_init_sync_method_options, + NULL, NULL, NULL + }, + { {"backslash_quote", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS, gettext_noop("Sets whether \"\\'\" is allowed in string literals."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 75cafa0b8d571..c6bcf6a8e4648 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -256,6 +256,8 @@ #dwb_max_segments = 4096 # segment hash capacity # (change requires restart) #dwb_retire_workers = 1 # retire worker processes +#dwb_retire_sync_method = syncfs # syncfs where supported (Linux; the + # default there), fsync elsewhere # (change requires restart) #dwb_batch_timeout_ms = 10ms # force-seal an open batch after this time #dwb_retire_interval_ms = 50ms # retire worker cycle diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index c761c6e65df09..5c45b454dc148 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -24,6 +24,7 @@ #include "access/xlogdefs.h" #include "catalog/pg_control.h" +#include "common/file_utils.h" #include "port/pg_crc32c.h" #include "storage/buf_internals.h" #include "storage/condition_variable.h" @@ -46,12 +47,28 @@ typedef enum DWB_ON_STALL_PANIC, } DWBOnStall; +/* + * GUC: dwb_retire_sync_method. Shares the DataDirSyncMethod values of + * recovery_init_sync_method (common/file_utils.h): "fsync" retires by + * fsyncing each touched data-file segment, "syncfs" makes whole file + * systems durable per retire round. syncfs is the default where the + * syscall exists: a shared random workload touches nearly every segment + * of a large table between rounds, and one syncfs replaces hundreds of + * per-segment fdatasync calls on the same file system. + */ +#ifdef HAVE_SYNCFS +#define DWB_RETIRE_SYNC_METHOD_DEFAULT DATA_DIR_SYNC_METHOD_SYNCFS +#else +#define DWB_RETIRE_SYNC_METHOD_DEFAULT DATA_DIR_SYNC_METHOD_FSYNC +#endif + /* GUC variables (defined in dwb_ctl.c) */ extern PGDLLIMPORT int io_torn_pages_protection; extern PGDLLIMPORT int dwb_num_batches; extern PGDLLIMPORT int dwb_batch_pages; extern PGDLLIMPORT int dwb_max_segments; extern PGDLLIMPORT int dwb_retire_workers; +extern PGDLLIMPORT int dwb_retire_sync_method; extern PGDLLIMPORT int dwb_batch_timeout_ms; extern PGDLLIMPORT int dwb_retire_interval_ms; extern PGDLLIMPORT bool dwb_writeback; @@ -78,6 +95,25 @@ extern PGDLLIMPORT int dwb_on_stall; #define DWB_WCLASS_EVICTION 0 #define DWB_WCLASS_BACKGROUND 1 +/* + * Why a batch was sealed. Purely diagnostic: per-class seal and page + * counters in DWCtl attribute batch turnover to its trigger, which is how + * a half-filled average (batch fsyncs paid for underfilled batches) is + * told apart from healthy overflow sealing. + */ +typedef enum DWBSealReason +{ + DWB_SEAL_OVERFLOW, /* a reservation ran past the last slot */ + DWB_SEAL_LONE, /* solo-stream fast seal: a lone waiter, or + * every page when there is no worker pool */ + DWB_SEAL_WAIT_TIMEOUT, /* a waiting writer hit dwb_batch_timeout_ms */ + DWB_SEAL_WORKER_TIMEOUT, /* a retire worker force-sealed on age */ + DWB_SEAL_BIN, /* a background bin flush sealed its batches */ + DWB_SEAL_FORCED, /* explicit DWBForceSealOpenBatch */ +} DWBSealReason; + +#define DWB_SEAL_NREASONS (DWB_SEAL_FORCED + 1) + #define DWB_DIR "pg_dwb" #define DWB_CONTROL_FILE DWB_DIR "/control" @@ -353,6 +389,9 @@ typedef struct DWCtl * timeout pace, not spin (see the * silent-probe-release rule in * DWBStagingRelease) */ + /* diagnostic seal accounting: [writer class][DWBSealReason] */ + pg_atomic_uint64 seal_count[DWB_NUM_WCLASSES][DWB_SEAL_NREASONS]; + pg_atomic_uint64 seal_pages[DWB_NUM_WCLASSES][DWB_SEAL_NREASONS]; ConditionVariable cv_want_batch[DWB_NUM_WCLASSES]; /* per-class "want a * batch" queue: both * staging and @@ -399,7 +438,7 @@ extern void DWBPublishImage(const DWBSlotRef *ref, const char *image, extern void DWBWaitBatchFsynced(const DWBSlotRef *ref); extern void DWBReleaseSlot(const DWBSlotRef *ref); extern bool DWBForceSealOpenBatch(int wclass); -extern bool DWBTrySealBatch(int batch_idx); +extern bool DWBTrySealBatch(int batch_idx, DWBSealReason reason); extern DWBatchState DWBGetBatchState(int batch_idx); /* internal; exported for test_dwb's stale-open regression test */ diff --git a/src/test/modules/test_dwb/expected/test_dwb.out b/src/test/modules/test_dwb/expected/test_dwb.out index 54773d786d255..d3a2787b18268 100644 --- a/src/test/modules/test_dwb/expected/test_dwb.out +++ b/src/test/modules/test_dwb/expected/test_dwb.out @@ -36,3 +36,13 @@ SELECT test_dwb_states(); free=16 allocated=0 sealed=0 written=0 fsynced=0 data_written=0 retiring=0 (1 row) +-- the cycles sealed deterministically: two overflow seals from the 40-page +-- run (2 x 16 slots) and two forced tail seals (5 + 8 slots) +SELECT wclass, reason, seals, pages FROM test_dwb_seal_stats() + WHERE seals > 0 ORDER BY wclass, reason; + wclass | reason | seals | pages +----------+----------+-------+------- + eviction | forced | 2 | 13 + eviction | overflow | 2 | 32 +(2 rows) + diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index 36835f11a1905..773fb68d004b7 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -53,6 +53,7 @@ tests += { 't/014_pg_upgrade.pl', 't/015_vectored_flush.pl', 't/016_bgwriter_bin.pl', + 't/017_syncfs_retire.pl', ], }, } diff --git a/src/test/modules/test_dwb/sql/test_dwb.sql b/src/test/modules/test_dwb/sql/test_dwb.sql index 50aa384632757..005e2d1619de8 100644 --- a/src/test/modules/test_dwb/sql/test_dwb.sql +++ b/src/test/modules/test_dwb/sql/test_dwb.sql @@ -16,3 +16,8 @@ SELECT test_dwb_ring_slots(true); -- and the ring is fully retired again SELECT test_dwb_states(); + +-- the cycles sealed deterministically: two overflow seals from the 40-page +-- run (2 x 16 slots) and two forced tail seals (5 + 8 slots) +SELECT wclass, reason, seals, pages FROM test_dwb_seal_stats() + WHERE seals > 0 ORDER BY wclass, reason; diff --git a/src/test/modules/test_dwb/t/002_flushbuffer.pl b/src/test/modules/test_dwb/t/002_flushbuffer.pl index fadb76e7c616a..ecb6656582398 100644 --- a/src/test/modules/test_dwb/t/002_flushbuffer.pl +++ b/src/test/modules/test_dwb/t/002_flushbuffer.pl @@ -141,4 +141,14 @@ or die 'timed out waiting for the ring to drain without a worker pool'; pass('ring drained to all-free without a worker pool'); +# The no-pool path seals every staged page right away through the +# solo-stream fast seal; the seal accounting must attribute them to it. +cmp_ok( + $node->safe_psql( + 'postgres', + "SELECT seals FROM test_dwb_seal_stats() " + . "WHERE wclass = 'eviction' AND reason = 'lone'"), + '>', 0, + 'no-pool seals are accounted as lone-writer fast seals'); + done_testing(); diff --git a/src/test/modules/test_dwb/t/004_retire_paths.pl b/src/test/modules/test_dwb/t/004_retire_paths.pl index 8f20f512603b6..882156172aeb3 100644 --- a/src/test/modules/test_dwb/t/004_retire_paths.pl +++ b/src/test/modules/test_dwb/t/004_retire_paths.pl @@ -26,6 +26,7 @@ dwb_batch_pages = 64 dwb_max_segments = 1024 dwb_retire_workers = 0 +dwb_retire_sync_method = fsync bgwriter_lru_maxpages = 0 checkpoint_timeout = 1h autovacuum = off diff --git a/src/test/modules/test_dwb/t/017_syncfs_retire.pl b/src/test/modules/test_dwb/t/017_syncfs_retire.pl new file mode 100644 index 0000000000000..e8917715e17fd --- /dev/null +++ b/src/test/modules/test_dwb/t/017_syncfs_retire.pl @@ -0,0 +1,74 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# dwb_retire_sync_method = syncfs: one syncfs() per retire round replaces +# the per-segment fdatasync protocol, and every batch that was RETIRING +# when the round began is freed wholesale. fsync stays ON so the rounds +# issue real syncfs() calls; the crash at the end proves the durability +# chain end to end. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_syncfs'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 1 +fsync = on +shared_buffers = 2MB +bgwriter_lru_maxpages = 0 +checkpoint_timeout = 1h +autovacuum = off +)); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); + +# syncfs is a build-time option (Linux); skip where the GUC cannot take it +if ($node->safe_psql('postgres', + "SELECT 'syncfs' = ANY(enumvals) FROM pg_settings " + . "WHERE name = 'dwb_retire_sync_method'") ne 't') +{ + plan skip_all => 'syncfs not supported by this build'; +} + +$node->safe_psql('postgres', + 'ALTER SYSTEM SET dwb_retire_sync_method = syncfs'); +$node->reload; +$node->poll_query_until('postgres', + "SELECT current_setting('dwb_retire_sync_method') = 'syncfs'") + or die 'timed out waiting for the syncfs retire method to apply'; + +# A workload well past shared_buffers streams evictions through the ring; +# the worker's syncfs rounds must keep freeing batches for it to finish. +$node->safe_psql( + 'postgres', q( + CREATE TABLE dwb_syncfs_t AS + SELECT g AS id, repeat('s', 300) AS filler + FROM generate_series(1, 20000) g; + UPDATE dwb_syncfs_t SET filler = repeat('f', 300) WHERE id % 5 = 0; +)); +is($node->safe_psql('postgres', 'SELECT count(*) FROM dwb_syncfs_t'), + '20000', 'workload survived the syncfs retire path'); + +# The worker alone must drain the ring: the poll only nudges the tail +# batch closed, all the freeing is the worker's wholesale rounds. +$node->poll_query_until('postgres', + "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " + . "test_dwb_states() LIKE 'free=16 %' END") + or die 'timed out waiting for syncfs rounds to drain the ring'; +pass('the worker drained the ring through syncfs rounds'); + +# Crash recovery on top of syncfs-retired data: intact. +$node->stop('immediate'); +$node->start; +is($node->safe_psql('postgres', 'SELECT count(*) FROM dwb_syncfs_t'), + '20000', 'data intact after crash recovery'); + +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index ea3ea67b9adbc..29179202833da 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -27,6 +27,11 @@ CREATE FUNCTION test_dwb_ring_wait_retries() RETURNS bigint STRICT AS 'MODULE_PATHNAME' LANGUAGE C; +CREATE FUNCTION test_dwb_seal_stats( + OUT wclass text, OUT reason text, OUT seals bigint, OUT pages bigint) + RETURNS SETOF record STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + CREATE FUNCTION test_dwb_leak(npages int, do_publish bool) RETURNS void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index c616d13134765..ef14be53c6a2d 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -22,6 +22,7 @@ #include "catalog/pg_tablespace_d.h" #include "common/relpath.h" #include "fmgr.h" +#include "funcapi.h" #include "miscadmin.h" #include "storage/bufpage.h" #include "storage/checksum.h" @@ -78,7 +79,7 @@ stage_one_page(const BufferTag *tag, const char *image, XLogRecPtr page_lsn, { DWBAcquireSlot(tag, DWB_WCLASS_EVICTION, use_resowner, ref); DWBPublishImage(ref, image, page_lsn); - if (!DWBTrySealBatch(ref->batch_idx)) + if (!DWBTrySealBatch(ref->batch_idx, DWB_SEAL_FORCED)) ereport(ERROR, (errmsg("could not seal the batch under test"))); DWBWaitBatchFsynced(ref); } @@ -352,6 +353,45 @@ test_dwb_ring_wait_retries(PG_FUNCTION_ARGS) PG_RETURN_INT64((int64) pg_atomic_read_u64(&DWBCtl->ring_wait_retries)); } +/* + * Cumulative seal accounting: one row per (writer class, seal reason) with + * the number of seal wins and the sum of slots the sealed batches carried. + * pages/seals is the average fill a reason is responsible for. + */ +PG_FUNCTION_INFO_V1(test_dwb_seal_stats); +Datum +test_dwb_seal_stats(PG_FUNCTION_ARGS) +{ + static const char *const wclass_names[DWB_NUM_WCLASSES] = { + "eviction", "background", + }; + static const char *const reason_names[DWB_SEAL_NREASONS] = { + "overflow", "lone", "wait_timeout", "worker_timeout", "bin", "forced", + }; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + + check_dwb_enabled(); + InitMaterializedSRF(fcinfo, 0); + + for (int c = 0; c < DWB_NUM_WCLASSES; c++) + for (int r = 0; r < DWB_SEAL_NREASONS; r++) + { + Datum values[4]; + bool nulls[4] = {0}; + + values[0] = CStringGetTextDatum(wclass_names[c]); + values[1] = CStringGetTextDatum(reason_names[r]); + values[2] = Int64GetDatum( + (int64) pg_atomic_read_u64(&DWBCtl->seal_count[c][r])); + values[3] = Int64GetDatum( + (int64) pg_atomic_read_u64(&DWBCtl->seal_pages[c][r])); + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + + return (Datum) 0; +} + PG_FUNCTION_INFO_V1(test_dwb_states); Datum test_dwb_states(PG_FUNCTION_ARGS) @@ -722,7 +762,7 @@ test_dwb_fill_segments(PG_FUNCTION_ARGS) DWBPublishImage(&refs[i], page, (XLogRecPtr) 0x7000000 + nsegs); nsegs++; } - if (!DWBTrySealBatch(refs[0].batch_idx)) + if (!DWBTrySealBatch(refs[0].batch_idx, DWB_SEAL_FORCED)) ereport(ERROR, (errmsg("could not seal a segment-fill batch"))); DWBWaitBatchFsynced(&refs[0]); for (int i = 0; i < dwb_batch_pages; i++) diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9dfd3beda80d8..c030870ded6c5 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -607,6 +607,7 @@ DWBControlFileData DWBOnStall DWBPendingRef DWBRetiringBatch +DWBSealReason DWBSegSyncSnap DWBSlotRef DWBStallState From d885dc161a5d2f16503a2e59498d6439f3acdc7d Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sun, 2 Aug 2026 13:26:11 +0300 Subject: [PATCH 29/52] Gate the wholesale retire round against concurrent duplicates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every retire worker wakes on the same cv_retire_wake broadcast, so with a pool larger than one the workers would all run the same global syncfs() round back to back, multiplying whole-file-system syncs for no benefit; the ring-full self-help could pile on top. Guard the round with LWLockConditionalAcquire on the new DWBSyncfsRoundLock: one process collects and syncs, losers return at once and are woken again by the winner's frees or the next publication. The batch collect runs under the lock, so a loser can never free batches some other round's syncfs did not cover. 017 now runs two retire workers and pins the single-admission property deterministically: a dwb-syncfs-round injection point inside the gate parks the winner, the loser must fall back asleep in its main loop, and the resumed round must free a batch parked with test_dwb_park(). While a process is parked inside the gate ALL wholesale retirement is frozen, so the scenario stays ring-quiet — no eviction workload runs until the point is detached (detach strictly before wakeup, as in 003). Also reunite the "change requires restart" comment with dwb_retire_workers in postgresql.conf.sample; the new SIGHUP parameter had been inserted between them. --- src/backend/storage/dwb/dwb_retire.c | 25 +++++++ .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/misc/postgresql.conf.sample | 2 +- src/include/storage/lwlocklist.h | 1 + .../modules/test_dwb/t/017_syncfs_retire.pl | 73 +++++++++++++++++-- 5 files changed, 93 insertions(+), 9 deletions(-) diff --git a/src/backend/storage/dwb/dwb_retire.c b/src/backend/storage/dwb/dwb_retire.c index 5b96d09df5400..c027aa065de97 100644 --- a/src/backend/storage/dwb/dwb_retire.c +++ b/src/backend/storage/dwb/dwb_retire.c @@ -61,6 +61,7 @@ #include "storage/md.h" #include "storage/sync.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/timestamp.h" #include "utils/wait_event.h" @@ -728,6 +729,28 @@ DWBRetireRoundSyncfs(void) int nretiring = 0; int freed = 0; + /* + * One round at a time: every retire worker wakes on the same broadcast, + * and the ring-full self-help can race the pool, but concurrent rounds + * would only duplicate a whole-file-system syncfs. A loser returns at + * once — the winner's round covers everything that was RETIRING when it + * collected, and batches published after that wake the pool again. The + * collect below runs under the lock, so no process can free a batch some + * other round's syncfs did not cover. An ERROR inside the round (e.g. an + * unreadable pg_tblspc) releases the gate in the unwind. + */ + if (!LWLockConditionalAcquire(DWBSyncfsRoundLock, LW_EXCLUSIVE)) + return 0; + + /* + * Test hook: proves the gate admits one process at a time. Sits before + * the collect, so a batch published while a test holds a round parked + * here is still picked up once the round resumes. NB: parking here + * freezes ALL wholesale retirement, so a test must not generate ring + * traffic while the point is armed. + */ + INJECTION_POINT("dwb-syncfs-round", NULL); + retiring = palloc(dwb_num_batches * sizeof(DWBRetiringBatch)); for (int i = 0; i < dwb_num_batches; i++) @@ -742,6 +765,7 @@ DWBRetireRoundSyncfs(void) if (nretiring == 0 || !DWBSyncfsAllFilesystems()) { + LWLockRelease(DWBSyncfsRoundLock); pfree(retiring); return 0; } @@ -788,6 +812,7 @@ DWBRetireRoundSyncfs(void) LWLockRelease(&batch->publish_lock); } + LWLockRelease(DWBSyncfsRoundLock); pfree(retiring); return freed; } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 4fb0658545642..289b389556a7e 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -367,6 +367,7 @@ AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." DWBRingOpen "Waiting to open a new double write buffer batch." DWBSegHash "Waiting to read or update the double write buffer segment hash table." DWBSelfSweep "Waiting to run the double write buffer self-help retirement sweep." +DWBSyncfsRound "Waiting to run a wholesale double write buffer retirement round." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index c6bcf6a8e4648..c7ab2b3842b5c 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -256,9 +256,9 @@ #dwb_max_segments = 4096 # segment hash capacity # (change requires restart) #dwb_retire_workers = 1 # retire worker processes + # (change requires restart) #dwb_retire_sync_method = syncfs # syncfs where supported (Linux; the # default there), fsync elsewhere - # (change requires restart) #dwb_batch_timeout_ms = 10ms # force-seal an open batch after this time #dwb_retire_interval_ms = 50ms # retire worker cycle #dwb_writeback = on # start kernel writeback after batch writes diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index be378e51a0b34..74006de5f1e67 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -87,3 +87,4 @@ PG_LWLOCK(53, AioWorkerSubmissionQueue) PG_LWLOCK(54, DWBRingOpen) PG_LWLOCK(55, DWBSegHash) PG_LWLOCK(56, DWBSelfSweep) +PG_LWLOCK(57, DWBSyncfsRound) diff --git a/src/test/modules/test_dwb/t/017_syncfs_retire.pl b/src/test/modules/test_dwb/t/017_syncfs_retire.pl index e8917715e17fd..b8e76c96a32e9 100644 --- a/src/test/modules/test_dwb/t/017_syncfs_retire.pl +++ b/src/test/modules/test_dwb/t/017_syncfs_retire.pl @@ -3,9 +3,10 @@ # dwb_retire_sync_method = syncfs: one syncfs() per retire round replaces # the per-segment fdatasync protocol, and every batch that was RETIRING -# when the round began is freed wholesale. fsync stays ON so the rounds -# issue real syncfs() calls; the crash at the end proves the durability -# chain end to end. +# when the round began is freed wholesale. Two workers prove the round +# gate (DWBSyncfsRoundLock) admits one process at a time; fsync stays ON +# so the rounds issue real syncfs() calls; the crash at the end proves +# the durability chain end to end. use strict; use warnings FATAL => 'all'; @@ -20,7 +21,7 @@ io_torn_pages_protection = double_writes dwb_num_batches = 16 dwb_batch_pages = 16 -dwb_retire_workers = 1 +dwb_retire_workers = 2 fsync = on shared_buffers = 2MB bgwriter_lru_maxpages = 0 @@ -45,8 +46,64 @@ "SELECT current_setting('dwb_retire_sync_method') = 'syncfs'") or die 'timed out waiting for the syncfs retire method to apply'; +$node->poll_query_until('postgres', + "SELECT count(*) = 2 FROM pg_stat_activity WHERE backend_type = 'dwb retire worker'" +) or die 'timed out waiting for the retire workers to start'; + +# --- the round gate admits exactly one process --------------------------- + +# The dwb-syncfs-round point sits INSIDE the trylock-guarded round, so a +# process can only park there after winning DWBSyncfsRoundLock. While a +# winner is parked there, ALL wholesale retirement is frozen, so this +# scenario must stay ring-quiet: one parked batch via test_dwb_park() and +# pg_stat_activity polls, no eviction workload. +SKIP: +{ + skip 'injection points not supported by this build', 3 + unless defined $ENV{enable_injection_points} + && $ENV{enable_injection_points} eq 'yes'; + + $node->safe_psql('postgres', 'CREATE EXTENSION injection_points'); + $node->safe_psql('postgres', + "SELECT injection_points_attach('dwb-syncfs-round', 'wait')"); + + # One RETIRING batch gives the winner's resumed round something to + # free; the client never touches the gate (park does not retire). + $node->safe_psql('postgres', 'SELECT test_dwb_park(99001)'); + + # The workers race the gate on their own interval clock: one parks at + # the point, the loser's tries all fail and put it back to sleep. + $node->wait_for_event('dwb retire worker', 'dwb-syncfs-round'); + pass('a worker won the gate and parked at the point'); + + $node->poll_query_until( + 'postgres', q( + SELECT count(*) FILTER (WHERE wait_event = 'dwb-syncfs-round') = 1 + AND count(*) FILTER (WHERE wait_event = 'DwbRetireMain') = 1 + FROM pg_stat_activity WHERE backend_type = 'dwb retire worker' + )) + or die 'timed out waiting for the gate loser to sleep in its main loop'; + pass('exactly one process is inside the syncfs round gate'); + + # Detach BEFORE waking: the winner re-enters the round on its next + # cycle, and with the point still attached it would park again with no + # wakeup left to release it. + $node->safe_psql('postgres', + "SELECT injection_points_detach('dwb-syncfs-round')"); + $node->safe_psql('postgres', + "SELECT injection_points_wakeup('dwb-syncfs-round')"); + + # The resumed round collects and frees the parked batch. + $node->poll_query_until('postgres', + "SELECT test_dwb_states() LIKE 'free=16 %'") + or die 'timed out waiting for the resumed round to free the batch'; + pass('the resumed round freed the parked batch'); +} + +# --- syncfs rounds drain a real workload --------------------------------- + # A workload well past shared_buffers streams evictions through the ring; -# the worker's syncfs rounds must keep freeing batches for it to finish. +# the workers' syncfs rounds must keep freeing batches for it to finish. $node->safe_psql( 'postgres', q( CREATE TABLE dwb_syncfs_t AS @@ -57,13 +114,13 @@ is($node->safe_psql('postgres', 'SELECT count(*) FROM dwb_syncfs_t'), '20000', 'workload survived the syncfs retire path'); -# The worker alone must drain the ring: the poll only nudges the tail -# batch closed, all the freeing is the worker's wholesale rounds. +# The workers alone must drain the ring: the poll only nudges the tail +# batch closed, all the freeing is the pool's wholesale rounds. $node->poll_query_until('postgres', "SELECT CASE WHEN test_dwb_force_seal() IS NOT NULL THEN " . "test_dwb_states() LIKE 'free=16 %' END") or die 'timed out waiting for syncfs rounds to drain the ring'; -pass('the worker drained the ring through syncfs rounds'); +pass('the workers drained the ring through syncfs rounds'); # Crash recovery on top of syncfs-retired data: intact. $node->stop('immediate'); From 853eaec05971fac3f01ed39683ca7c01d0cc31cb Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sun, 2 Aug 2026 17:38:41 +0300 Subject: [PATCH 30/52] Suppress the lone-writer fast seal while the class is hot (Stage 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seal accounting on the perf stand showed 54% of eviction batches sealed by the lone-writer fast seal at 1.33 slots average, while overflow-sealed batches carry exactly 64: under a dense concurrent stream the FIRST writer of every freshly opened batch publishes within microseconds and reaches the fsync wait while ref_count is still 1, so the fast seal meant for sequential streams halves the ring into one-page batches and doubles the batch-file fsync count. Gate the fast seal on the class being quiet: every overflow seal stamps the class's last-overflow timestamp (twice — at the seal win so concurrent writers see the class as hot during the leader write, and after the write so the window survives a slow batch fdatasync), and a lone writer skips the fast seal while DWBClassIsHot sees the stamp as younger than dwb_batch_timeout_ms. A stamp from the future — a backward system-clock step — reads as quiet, keeping the immediate seal instead of taxing sequential streams until the clock catches up. A wrongly suppressed seal costs at most the window: the waiter's own timeout seal fires after it in every mode, and the retire workers' force-seal backs it up whenever a pool is configured. Sequential streams see no overflow seals and keep the immediate seal, as does the unconditional no-pool seal in DWBStagePageWrite. The regress test pins all three sides deterministically through the seal counters: a quiet class fast-seals one staged page (lone +1); a hot-window driver overflows a batch in the same backend and enters the wait on the next batch's first slot microseconds later — the fast seal must give way to the waiter's timeout seal (overflow +1, wait_timeout +1, lone +0); and a planted future stamp must read as quiet through the same DWBClassIsHot helper and keep the fast seal immediate. dwb_batch_timeout_ms is raised to 200ms in the regress configuration so the in-process stamp-to-check gap has a wide margin over the threshold on slow machines. --- doc/src/sgml/config.sgml | 7 +- src/backend/storage/dwb/dwb.c | 58 ++++++++++- src/backend/storage/dwb/dwb_ctl.c | 2 + src/include/storage/dwb.h | 9 ++ .../modules/test_dwb/expected/test_dwb.out | 96 +++++++++++++++++++ src/test/modules/test_dwb/sql/test_dwb.sql | 34 +++++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 12 +++ src/test/modules/test_dwb/test_dwb.c | 82 ++++++++++++++++ src/test/modules/test_dwb/test_dwb.conf | 3 + 9 files changed, 301 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 1e38f1ba15b92..94e0da1b2b52b 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3593,7 +3593,12 @@ include_dir 'conf.d' Maximum time an open batch may collect pages before it is sealed and written even if not full, bounding the latency a lone page - flush can spend waiting for company. + flush can spend waiting for company. A lone page flush seals its + batch immediately while its writer class is quiet; after a recent + full-batch seal — the sign of a dense concurrent stream — it waits + out this window instead, letting the batch fill. With + set to 0 and in + single-user mode a lone flush always seals immediately. If this value is specified without units, it is taken as milliseconds. The default is 10 milliseconds. This parameter can only be set in the postgresql.conf diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 6aa64bdc2b327..642c26636a69c 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -568,6 +568,17 @@ DWBSealBatch(int batch_idx, DWBSealReason reason) pg_atomic_fetch_add_u64(&DWBCtl->seal_count[wclass][reason], 1); pg_atomic_fetch_add_u64(&DWBCtl->seal_pages[wclass][reason], capped); + /* + * An overflow seal marks the class as HOT: writers are streaming in + * faster than a batch fills, so a lone writer in a fresh batch must not + * fast-seal it (see DWBWaitBatchFsynced). Stamped again after the leader + * write below: the hot window has to survive a slow batch fdatasync, or + * the first writer after it would find a stale stamp. + */ + if (reason == DWB_SEAL_OVERFLOW) + pg_atomic_write_u64(&DWBCtl->last_overflow_seal[wclass], + (uint64) GetCurrentTimestamp()); + expected = DWB_ALLOCATED; if (!pg_atomic_compare_exchange_u32(&batch->state, &expected, DWB_SEALED)) elog(PANIC, "DWB batch %d sealed in unexpected state %u", @@ -602,6 +613,11 @@ DWBSealBatch(int batch_idx, DWBSealReason reason) END_CRIT_SECTION(); + /* the hot window starts over once the overflow's fdatasync is done */ + if (reason == DWB_SEAL_OVERFLOW) + pg_atomic_write_u64(&DWBCtl->last_overflow_seal[wclass], + (uint64) GetCurrentTimestamp()); + if (pg_atomic_fetch_sub_u32(&batch->ref_count, 1) == 1) DWBFinishBatchData(batch); @@ -911,6 +927,27 @@ DWBPublishImage(const DWBSlotRef *ref, const char *image, XLogRecPtr page_lsn) ConditionVariableBroadcast(&batch->cv_state); } +/* + * Is the writer class's demand hot — was its last overflow seal younger + * than the rendezvous window? Hot only when the clock reads at or past + * the stamp: a stamp from the future (a backward system-clock step) must + * read as QUIET, or the immediate lone seal would stay disabled until the + * clock catches up, taxing every write of a sequential stream with the + * timeout. Exported for the test module, which plants a future stamp to + * pin exactly that branch. + */ +bool +DWBClassIsHot(int wclass) +{ + TimestampTz stamp; + TimestampTz now; + + stamp = (TimestampTz) pg_atomic_read_u64(&DWBCtl->last_overflow_seal[wclass]); + now = GetCurrentTimestamp(); + return now >= stamp && + !TimestampDifferenceExceeds(stamp, now, dwb_batch_timeout_ms); +} + /* * Wait until the batch's DWB copy is durable. The caller holds a batch * ref, so the batch cannot be retired or reused under us. @@ -936,9 +973,28 @@ DWBWaitBatchFsynced(const DWBSlotRef *ref) * seal right away; under concurrency ref_count > 1 keeps the rendezvous * window open for the timeout. A racing second writer merely bounces to * the next batch — sealing is valid at any moment. + * + * The fast seal only applies while the class is QUIET. Under a dense + * concurrent stream the FIRST writer of every freshly opened batch also + * finds ref_count == 1 — it published within microseconds and nobody + * joined yet — and fast-sealing there halves the ring into one-page + * batches (54% of eviction batches at 1.33 slots, measured). A class + * whose last overflow seal is younger than the rendezvous window is + * clearly hot: skip the fast seal and let the batch fill. If the stream + * dies right here, the timeout seal below and the retire workers' + * force-seal still fire after dwb_batch_timeout_ms. The class bit of a + * held-ref batch is stable (reopen is fenced by the ref); the stamp is + * advisory, so a stale read just mis-decides one seal. */ if (pg_atomic_read_u32(&batch->ref_count) == 1) - (void) DWBTrySealBatch(ref->batch_idx, DWB_SEAL_LONE); + { + int wclass; + + wclass = (pg_atomic_read_u32(&batch->next_slot_idx) & DWB_WCLASS_BIT) ? + DWB_WCLASS_BACKGROUND : DWB_WCLASS_EVICTION; + if (!DWBClassIsHot(wclass)) + (void) DWBTrySealBatch(ref->batch_idx, DWB_SEAL_LONE); + } ConditionVariablePrepareToSleep(&batch->cv_state); while (pg_atomic_read_u32(&batch->state) < DWB_FSYNCED) diff --git a/src/backend/storage/dwb/dwb_ctl.c b/src/backend/storage/dwb/dwb_ctl.c index b132039f448b2..26820050e1cd2 100644 --- a/src/backend/storage/dwb/dwb_ctl.c +++ b/src/backend/storage/dwb/dwb_ctl.c @@ -100,6 +100,8 @@ DWBShmemInit(void) pg_atomic_init_u64(&DWBCtl->seal_count[c][r], 0); pg_atomic_init_u64(&DWBCtl->seal_pages[c][r], 0); } + for (int c = 0; c < DWB_NUM_WCLASSES; c++) + pg_atomic_init_u64(&DWBCtl->last_overflow_seal[c], 0); for (int c = 0; c < DWB_NUM_WCLASSES; c++) ConditionVariableInit(&DWBCtl->cv_want_batch[c]); ConditionVariableInit(&DWBCtl->cv_retire_wake); diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 5c45b454dc148..1e2a8f9ff9a85 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -392,6 +392,14 @@ typedef struct DWCtl /* diagnostic seal accounting: [writer class][DWBSealReason] */ pg_atomic_uint64 seal_count[DWB_NUM_WCLASSES][DWB_SEAL_NREASONS]; pg_atomic_uint64 seal_pages[DWB_NUM_WCLASSES][DWB_SEAL_NREASONS]; + + /* + * TimestampTz of the class's last overflow seal: the "demand is hot" + * marker that suppresses the lone-writer fast seal (see + * DWBWaitBatchFsynced). Advisory — read and written without barriers; + * a stale value mis-decides at most one seal in either direction. + */ + pg_atomic_uint64 last_overflow_seal[DWB_NUM_WCLASSES]; ConditionVariable cv_want_batch[DWB_NUM_WCLASSES]; /* per-class "want a * batch" queue: both * staging and @@ -439,6 +447,7 @@ extern void DWBWaitBatchFsynced(const DWBSlotRef *ref); extern void DWBReleaseSlot(const DWBSlotRef *ref); extern bool DWBForceSealOpenBatch(int wclass); extern bool DWBTrySealBatch(int batch_idx, DWBSealReason reason); +extern bool DWBClassIsHot(int wclass); extern DWBatchState DWBGetBatchState(int batch_idx); /* internal; exported for test_dwb's stale-open regression test */ diff --git a/src/test/modules/test_dwb/expected/test_dwb.out b/src/test/modules/test_dwb/expected/test_dwb.out index d3a2787b18268..3e1fa377cd942 100644 --- a/src/test/modules/test_dwb/expected/test_dwb.out +++ b/src/test/modules/test_dwb/expected/test_dwb.out @@ -46,3 +46,99 @@ SELECT wclass, reason, seals, pages FROM test_dwb_seal_stats() eviction | overflow | 2 | 32 (2 rows) +-- lone-writer fast seal in a QUIET class: get clear of the cycles' overflow +-- stamps first, then a single staged page must seal immediately as "lone" +SELECT pg_sleep(0.3); + pg_sleep +---------- + +(1 row) + +CREATE TEMP TABLE seal_before_quiet AS SELECT * FROM test_dwb_seal_stats(); +SELECT test_dwb_stage_lone_wait(); + test_dwb_stage_lone_wait +-------------------------- + +(1 row) + +SELECT s.wclass, s.reason, s.seals - b.seals AS dseals, s.pages - b.pages AS dpages + FROM test_dwb_seal_stats() s JOIN seal_before_quiet b USING (wclass, reason) + WHERE s.seals <> b.seals ORDER BY s.wclass, s.reason; + wclass | reason | dseals | dpages +----------+--------+--------+-------- + eviction | lone | 1 | 1 +(1 row) + +SELECT test_dwb_retire() >= 0 AS drained; + drained +--------- + t +(1 row) + +-- HOT class: an overflow seal microseconds before the lone attempt must +-- suppress the fast seal into the waiter's timeout seal +CREATE TEMP TABLE seal_before_hot AS SELECT * FROM test_dwb_seal_stats(); +SELECT test_dwb_overflow_lone_wait(); + test_dwb_overflow_lone_wait +----------------------------- + +(1 row) + +SELECT s.wclass, s.reason, s.seals - b.seals AS dseals, s.pages - b.pages AS dpages + FROM test_dwb_seal_stats() s JOIN seal_before_hot b USING (wclass, reason) + WHERE s.seals <> b.seals ORDER BY s.wclass, s.reason; + wclass | reason | dseals | dpages +----------+--------------+--------+-------- + eviction | overflow | 1 | 16 + eviction | wait_timeout | 1 | 1 +(2 rows) + +SELECT test_dwb_retire() >= 0 AS drained; + drained +--------- + t +(1 row) + +-- the hot test reads the stamp through the same helper: a fresh stamp is +-- hot, a stamp from the FUTURE (a backward clock step) must read as quiet +-- and keep the fast seal immediate +SELECT test_dwb_set_overflow_stamp(0) AS hot_now; + hot_now +--------- + t +(1 row) + +SELECT test_dwb_set_overflow_stamp(60000) AS hot_future; + hot_future +------------ + f +(1 row) + +CREATE TEMP TABLE seal_before_future AS SELECT * FROM test_dwb_seal_stats(); +SELECT test_dwb_stage_lone_wait(); + test_dwb_stage_lone_wait +-------------------------- + +(1 row) + +SELECT s.wclass, s.reason, s.seals - b.seals AS dseals, s.pages - b.pages AS dpages + FROM test_dwb_seal_stats() s JOIN seal_before_future b USING (wclass, reason) + WHERE s.seals <> b.seals ORDER BY s.wclass, s.reason; + wclass | reason | dseals | dpages +----------+--------+--------+-------- + eviction | lone | 1 | 1 +(1 row) + +SELECT test_dwb_retire() >= 0 AS drained; + drained +--------- + t +(1 row) + +-- and the ring is idle again +SELECT test_dwb_states(); + test_dwb_states +---------------------------------------------------------------------------- + free=16 allocated=0 sealed=0 written=0 fsynced=0 data_written=0 retiring=0 +(1 row) + diff --git a/src/test/modules/test_dwb/sql/test_dwb.sql b/src/test/modules/test_dwb/sql/test_dwb.sql index 005e2d1619de8..a583ac543b5bb 100644 --- a/src/test/modules/test_dwb/sql/test_dwb.sql +++ b/src/test/modules/test_dwb/sql/test_dwb.sql @@ -21,3 +21,37 @@ SELECT test_dwb_states(); -- run (2 x 16 slots) and two forced tail seals (5 + 8 slots) SELECT wclass, reason, seals, pages FROM test_dwb_seal_stats() WHERE seals > 0 ORDER BY wclass, reason; + +-- lone-writer fast seal in a QUIET class: get clear of the cycles' overflow +-- stamps first, then a single staged page must seal immediately as "lone" +SELECT pg_sleep(0.3); +CREATE TEMP TABLE seal_before_quiet AS SELECT * FROM test_dwb_seal_stats(); +SELECT test_dwb_stage_lone_wait(); +SELECT s.wclass, s.reason, s.seals - b.seals AS dseals, s.pages - b.pages AS dpages + FROM test_dwb_seal_stats() s JOIN seal_before_quiet b USING (wclass, reason) + WHERE s.seals <> b.seals ORDER BY s.wclass, s.reason; +SELECT test_dwb_retire() >= 0 AS drained; + +-- HOT class: an overflow seal microseconds before the lone attempt must +-- suppress the fast seal into the waiter's timeout seal +CREATE TEMP TABLE seal_before_hot AS SELECT * FROM test_dwb_seal_stats(); +SELECT test_dwb_overflow_lone_wait(); +SELECT s.wclass, s.reason, s.seals - b.seals AS dseals, s.pages - b.pages AS dpages + FROM test_dwb_seal_stats() s JOIN seal_before_hot b USING (wclass, reason) + WHERE s.seals <> b.seals ORDER BY s.wclass, s.reason; +SELECT test_dwb_retire() >= 0 AS drained; + +-- the hot test reads the stamp through the same helper: a fresh stamp is +-- hot, a stamp from the FUTURE (a backward clock step) must read as quiet +-- and keep the fast seal immediate +SELECT test_dwb_set_overflow_stamp(0) AS hot_now; +SELECT test_dwb_set_overflow_stamp(60000) AS hot_future; +CREATE TEMP TABLE seal_before_future AS SELECT * FROM test_dwb_seal_stats(); +SELECT test_dwb_stage_lone_wait(); +SELECT s.wclass, s.reason, s.seals - b.seals AS dseals, s.pages - b.pages AS dpages + FROM test_dwb_seal_stats() s JOIN seal_before_future b USING (wclass, reason) + WHERE s.seals <> b.seals ORDER BY s.wclass, s.reason; +SELECT test_dwb_retire() >= 0 AS drained; + +-- and the ring is idle again +SELECT test_dwb_states(); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index 29179202833da..16a801c2e1709 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -32,6 +32,18 @@ CREATE FUNCTION test_dwb_seal_stats( RETURNS SETOF record STRICT AS 'MODULE_PATHNAME' LANGUAGE C; +CREATE FUNCTION test_dwb_stage_lone_wait() + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_overflow_lone_wait() + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_set_overflow_stamp(delta_ms int) + RETURNS bool STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + CREATE FUNCTION test_dwb_leak(npages int, do_publish bool) RETURNS void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index ef14be53c6a2d..b3e87d81f575d 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -32,6 +32,7 @@ #include "storage/sync.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" +#include "utils/timestamp.h" #include "varatt.h" PG_MODULE_MAGIC; @@ -771,6 +772,87 @@ test_dwb_fill_segments(PG_FUNCTION_ARGS) PG_RETURN_INT32(nsegs); } +/* + * Acquire and publish ONE synthetic page and enter the fsync wait WITHOUT + * sealing first: the only SQL driver of the lone-writer fast-seal path + * (stage_one_page force-seals and never reaches it). In a quiet class the + * wait returns through the immediate lone seal; in a hot one it sleeps + * until the timeout seal. + */ +PG_FUNCTION_INFO_V1(test_dwb_stage_lone_wait); +Datum +test_dwb_stage_lone_wait(PG_FUNCTION_ARGS) +{ + BufferTag tag = make_tag(1, 93000, 0); + DWBSlotRef ref; + static char page[BLCKSZ]; + + check_dwb_enabled(); + + memset(page, 'Q', BLCKSZ); + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &ref); + DWBPublishImage(&ref, page, (XLogRecPtr) 0xA000000); + DWBWaitBatchFsynced(&ref); + DWBReleaseSlot(&ref); + PG_RETURN_VOID(); +} + +/* + * Plant the eviction class's last-overflow-seal stamp delta_ms from now. + * A positive delta puts the stamp in the FUTURE — the backward-clock-step + * shape that DWBClassIsHot must read as quiet. + */ +PG_FUNCTION_INFO_V1(test_dwb_set_overflow_stamp); +Datum +test_dwb_set_overflow_stamp(PG_FUNCTION_ARGS) +{ + int32 delta_ms = PG_GETARG_INT32(0); + TimestampTz stamp; + + check_dwb_enabled(); + stamp = GetCurrentTimestamp() + (TimestampTz) delta_ms * 1000; + pg_atomic_write_u64(&DWBCtl->last_overflow_seal[DWB_WCLASS_EVICTION], + (uint64) stamp); + PG_RETURN_BOOL(DWBClassIsHot(DWB_WCLASS_EVICTION)); +} + +/* + * The hot-window driver: fill and overflow one batch in this backend — the + * overflow seal runs the leader write and the batch fdatasync synchronously + * right here and leaves the extra slot in the next batch — then IMMEDIATELY + * enter the fsync wait on that next-batch ref while its ref_count is 1. + * The stamp-to-check gap is a few in-process reads after the overflow's + * fdatasync (the post-write re-stamp), so the hot suppression must turn + * the would-be lone seal into the waiter's timeout seal. + */ +PG_FUNCTION_INFO_V1(test_dwb_overflow_lone_wait); +Datum +test_dwb_overflow_lone_wait(PG_FUNCTION_ARGS) +{ + DWBSlotRef refs[DWB_BATCH_MAX_PAGES + 1]; + static char page[BLCKSZ]; + int npages; + + check_dwb_enabled(); + npages = dwb_batch_pages + 1; + + for (int i = 0; i < npages; i++) + { + BufferTag tag = make_tag(1, (Oid) (93100 + i), 0); + + memset(page, 'H', BLCKSZ); + DWBAcquireSlot(&tag, DWB_WCLASS_EVICTION, false, &refs[i]); + DWBPublishImage(&refs[i], page, (XLogRecPtr) 0xB000000 + i); + } + if (refs[npages - 1].batch_idx == refs[0].batch_idx) + ereport(ERROR, + (errmsg("overflow did not move the extra slot to a fresh batch"))); + + DWBWaitBatchFsynced(&refs[npages - 1]); + wait_and_release(refs, npages); + PG_RETURN_VOID(); +} + PG_FUNCTION_INFO_V1(test_dwb_force_seal); Datum test_dwb_force_seal(PG_FUNCTION_ARGS) diff --git a/src/test/modules/test_dwb/test_dwb.conf b/src/test/modules/test_dwb/test_dwb.conf index 9a5fc08765b10..6dd4c21593b5c 100644 --- a/src/test/modules/test_dwb/test_dwb.conf +++ b/src/test/modules/test_dwb/test_dwb.conf @@ -3,5 +3,8 @@ dwb_num_batches = 16 dwb_batch_pages = 16 # keep sealing and retirement under the test's control dwb_retire_workers = 0 +# wide margin for the hot/quiet lone-seal scenarios: the hot check must run +# within this window of its overflow, the quiet one clearly outside it +dwb_batch_timeout_ms = 200ms bgwriter_lru_maxpages = 0 autovacuum = off From 23692129dcc257c7bd978d3580e614a16c867fb4 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sun, 2 Aug 2026 23:22:46 +0300 Subject: [PATCH 31/52] Scale background LRU cleaning with a cleaner worker pool (Stage 5) With the double write buffer, one bgwriter executing its flush bins serially cannot clean more than ~11K pages/s: each bin waits out a batch fdatasync before the data-file writes. Backends then evict dirty buffers themselves and pay the full batch rendezvous (~2 ms per page against ~50 us of a plain page-cache write) on every eviction. Keep the bgwriter as the only LRU scanner and pacing estimator (the StrategySyncStart allocation counter is consumed on read, so the estimator cannot be split), but hand the bins to a small shared-memory queue served by dwb_cleaner_workers background workers. Queue entries are hints, not obligations: the pool executes them through FlushBufferBin's new opportunistic mode, which reclassifies every member under the buffer header lock with the scan's LRU-candidate predicate and drops - never waits on - members that became hot, went clean, lost BM_PERMANENT or are busy with somebody's I/O. The mandatory mode of the existing callers (checkpointer, bgwriter self-flush) is unchanged. A full or busy queue fails the enqueue and the bgwriter flushes the bin itself, degrading exactly to the pool-less behavior; nothing needs draining on shutdown. The bgwriter_lru_maxpages budget now caps pages issued per round (queued plus self-written) while buf_written_clean keeps counting actual writes: the pool's completions are folded in once per round and the self-flushed bins are counted at the flush, so pg_stat_bgwriter stays "pages written by LRU cleaning" no matter which process wrote them. The counters reconcile exactly in error-free operation; a worker error mid-bin abandons the bin's remainder, which stays dirty for a later scan. The pool lands in the background writer class of the ring (a plain B_BG_WORKER would classify as eviction and fight the clients for the eviction reserve). Workers use the aux-process resource owner so an error mid-bin releases pins and buffer I-O through the existing repair path, exit through die() on SIGTERM - an exit code of 0 would unregister the worker for good and one stray terminate would permanently shrink the pool - and flush their I/O statistics before each queue sleep. t/018_cleaners.pl pins the queue protocol down with one-page claims driven straight into the queue: a cooled dirty page is written, a page pinned by another session and an already-clean duplicate are skipped (the pin helper survives rollbacks via a transaction callback), a real workload flows scan -> queue -> pool with the writes visible in the background DWB class, pg_stat_io and pg_stat_bgwriter, a parked pool lets claims overflow the queue and the bgwriter cleans solo, the drained queue reconciles enqueued = written + skipped, a terminated worker is restarted keeping the pool size, and an oversized pool is refused at startup while free worker slots are counted. Parked workers are woken one wakeup per waiter - a worker left sleeping at a detached injection point would touch the detached segment on exit. --- doc/src/sgml/config.sgml | 36 ++ doc/src/sgml/monitoring.sgml | 7 +- src/backend/postmaster/bgworker.c | 3 + src/backend/postmaster/postmaster.c | 3 + src/backend/storage/buffer/bufmgr.c | 112 +++-- src/backend/storage/dwb/Makefile | 1 + src/backend/storage/dwb/dwb.c | 12 +- src/backend/storage/dwb/dwb_cleaner.c | 342 +++++++++++++++ src/backend/storage/dwb/dwb_ctl.c | 4 + src/backend/storage/dwb/meson.build | 1 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/misc/guc_tables.c | 13 + src/backend/utils/misc/postgresql.conf.sample | 3 + src/include/storage/buf_internals.h | 8 + src/include/storage/dwb.h | 68 +++ src/include/storage/lwlocklist.h | 1 + src/test/modules/test_dwb/meson.build | 1 + src/test/modules/test_dwb/t/018_cleaners.pl | 407 ++++++++++++++++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 18 + src/test/modules/test_dwb/test_dwb.c | 193 +++++++++ src/tools/pgindent/typedefs.list | 2 + 21 files changed, 1204 insertions(+), 33 deletions(-) create mode 100644 src/backend/storage/dwb/dwb_cleaner.c create mode 100644 src/test/modules/test_dwb/t/018_cleaners.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 94e0da1b2b52b..2212717134d70 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3554,6 +3554,42 @@ include_dir 'conf.d' + + dwb_cleaner_workers (integer) + + dwb_cleaner_workers configuration parameter + + + + + Number of background workers that execute the flush bins produced + by the background writer's LRU scan. Each bin is one double write + buffer batch write followed by its sync and the data-file writes; + a single background writer executing them serially cannot clean + more than a few thousand pages per second, and backends then evict + dirty buffers themselves at full double write buffer latency. + With a pool the scan keeps running in the background writer while + the bins are executed concurrently, and backends find clean + buffers instead. The workers consume + slots. Setting it to 0 + (the default) disables the pool and the background writer flushes + its bins itself. + This parameter can only be set at server start. + + + The pool scales execution only; how much the LRU scan issues per + round is still governed by + , + and + . With their default + values the pool mostly idles, so raise them together with this + setting. Pages written by the pool are counted in + pg_stat_bgwriter.buffers_clean + as if the background writer had written them itself. + + + + dwb_retire_sync_method (enum) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index c041349f4ca16..d86e7aa51e8d1 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -3067,7 +3067,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage buffers_clean bigint - Number of buffers written by the background writer + Number of buffers written by the background writer's LRU cleaning, + including buffers its scan handed to the cleaner worker pool + (see ) @@ -3077,7 +3079,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage Number of times the background writer stopped a cleaning - scan because it had written too many buffers + scan because it had issued too many buffers, counting both its own + writes and bins handed to the cleaner worker pool diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c index aa13f85466260..ec766f1a2bcf4 100644 --- a/src/backend/postmaster/bgworker.c +++ b/src/backend/postmaster/bgworker.c @@ -128,6 +128,9 @@ static const struct { "DWBRetireWorkerMain", DWBRetireWorkerMain }, + { + "DWBCleanerWorkerMain", DWBCleanerWorkerMain + }, { "ApplyWorkerMain", ApplyWorkerMain }, diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index 2ec284280a106..c3e98905f7227 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -934,6 +934,9 @@ PostmasterMain(int argc, char *argv[]) */ DWBRetireWorkersRegister(); + /* And the double write buffer cleaner pool feeding off the bgwriter. */ + DWBCleanerWorkersRegister(); + /* * process any libraries that should be preloaded at postmaster start */ diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index b718203b839b4..9cf0e1a15fbe3 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -85,16 +85,6 @@ #define BUF_BINNABLE 0x04 /* would-write candidate for the * vectored DWB flush bin */ -/* - * Bin size cap for the vectored background flush (FlushBufferBin, used by - * the checkpointer's BufferSync and the bgwriter's LRU scan): the flush - * holds a pin, a shared content lock and BM_IO_IN_PROGRESS per bin member - * at once, so the cap must leave MAX_SIMUL_LWLOCKS (200) plenty of - * headroom. 64 matches the default dwb_batch_pages; larger batch_pages - * settings seal their batches at bin-sized fills. - */ -#define DWB_FLUSH_BIN_MAX 64 - #define RELS_BSEARCH_THRESHOLD 20 /* @@ -533,8 +523,6 @@ static void UnpinBuffer(BufferDesc *buf); static void UnpinBufferNoOwner(BufferDesc *buf); static void BufferSync(int flags); static uint32 WaitBufHdrUnlocked(BufferDesc *buf); -static int FlushBufferBin(const int *buf_ids, int nbuf, - WritebackContext *wb_context); static int BgSyncPeekBuffer(int buf_id); static int SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context); @@ -3605,7 +3593,7 @@ BufferSync(int flags) if (dwb_bin_n == dwb_bin_size) { int nw = FlushBufferBin(dwb_bin, dwb_bin_n, - &wb_context); + false, &wb_context); PendingCheckpointerStats.buffers_written += nw; num_written += nw; @@ -3653,7 +3641,7 @@ BufferSync(int flags) if (dwb_bin_n > 0) { int nw = FlushBufferBin(dwb_bin, dwb_bin_n, - &wb_context); + false, &wb_context); PendingCheckpointerStats.buffers_written += nw; num_written += nw; @@ -3729,12 +3717,14 @@ BgBufferSync(WritebackContext *wb_context) /* Variables for the scanning loop proper */ int num_to_scan; int num_written; + int num_issued; int reusable_buffers; /* Vectored DWB flush bin (bin_size stays 0 without the DWB) */ int bin[DWB_FLUSH_BIN_MAX]; int bin_n = 0; int bin_size = 0; + bool use_cleaners; /* Variables for final smoothed_density update */ long new_strategy_delta; @@ -3749,6 +3739,16 @@ BgBufferSync(WritebackContext *wb_context) /* Report buffer alloc counts to pgstat */ PendingBgWriterStats.buf_alloc += recent_alloc; + /* + * Fold the cleaner pool's completed writes into buf_written_clean: + * pg_stat_bgwriter keeps counting pages written by LRU cleaning no matter + * which process executed the write. + */ + use_cleaners = DWBCleanersActive(); + if (use_cleaners) + PendingBgWriterStats.buf_written_clean += + DWBCleanerFetchPoolWritten(); + /* * If we're not running the LRU scan, just stop after doing the stats * stuff. We mark the saved state invalid so that we can recover sanely @@ -3914,6 +3914,7 @@ BgBufferSync(WritebackContext *wb_context) num_to_scan = bufs_to_lap; num_written = 0; + num_issued = 0; reusable_buffers = reusable_buffers_est; /* @@ -3921,7 +3922,12 @@ BgBufferSync(WritebackContext *wb_context) * into bins and flushed as one batch each: one batch write and one * fdatasync cover the whole bin instead of one per page (the LRU scan's * scattered singleton writes otherwise degenerate to lone-writer batches; - * see FlushBufferBin). + * see FlushBufferBin). With a cleaner pool the bins are handed to the + * pool's queue instead, and only when that fails (queue full: the pool is + * the bottleneck) flushed here. The bgwriter_lru_maxpages budget caps + * the pages ISSUED per round — queued and self-written together — + * while buf_written_clean counts actual writes only (the pool's + * completions are folded in at the top of the next round). */ if (DWBIsEnabled()) bin_size = Min(dwb_batch_pages, DWB_FLUSH_BIN_MAX); @@ -3945,6 +3951,7 @@ BgBufferSync(WritebackContext *wb_context) { reusable_buffers++; num_written++; + num_issued++; } else if (sync_state & BUF_REUSABLE) reusable_buffers++; @@ -3958,18 +3965,28 @@ BgBufferSync(WritebackContext *wb_context) /* * Flush a full bin, and any partial one that already covers the - * remaining write budget: the cap check below must see the true - * written count, not a deferred bin. + * remaining issue budget: the cap check below must see the true + * issued count, not a deferred bin. */ if (bin_n > 0 && (bin_n == bin_size || - num_written + bin_n >= bgwriter_lru_maxpages)) + num_issued + bin_n >= bgwriter_lru_maxpages)) { - num_written += FlushBufferBin(bin, bin_n, wb_context); + if (use_cleaners && DWBCleanerEnqueueBin(bin, bin_n)) + num_issued += bin_n; + else + { + int nw = FlushBufferBin(bin, bin_n, false, wb_context); + + num_written += nw; + num_issued += nw; + if (use_cleaners) + DWBCleanerCountSelfFlush(); + } bin_n = 0; } - if (num_written >= bgwriter_lru_maxpages) + if (num_issued >= bgwriter_lru_maxpages) { PendingBgWriterStats.maxwritten_clean++; break; @@ -3977,7 +3994,19 @@ BgBufferSync(WritebackContext *wb_context) } if (bin_n > 0) - num_written += FlushBufferBin(bin, bin_n, wb_context); + { + if (use_cleaners && DWBCleanerEnqueueBin(bin, bin_n)) + num_issued += bin_n; + else + { + int nw = FlushBufferBin(bin, bin_n, false, wb_context); + + num_written += nw; + num_issued += nw; + if (use_cleaners) + DWBCleanerCountSelfFlush(); + } + } PendingBgWriterStats.buf_written_clean += num_written; @@ -4169,9 +4198,25 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context) * buffer that became recently-used after the caller picked it is the same * benign race the per-page paths have between their check and their write. * Returns the number of buffers written. + * + * All of the above describes the mandatory mode (opportunistic = false): + * the caller's bins are fresh and every member must be flushed or handed + * to the blocking per-page fallback — checkpointer semantics. The + * cleaner worker pool executes bins that sat in a queue for arbitrarily + * long, so it passes opportunistic = true: each member is reclassified + * under the buffer header lock with the LRU-candidate predicate of the + * scan that produced it (unpinned, unused, valid, dirty — see + * BgSyncPeekBuffer), and a member that fails the predicate, lost + * BM_PERMANENT, or cannot be claimed without waiting is dropped instead + * of written: the page stays dirty for the next scan pass or checkpoint, + * and the cleaner never blocks on somebody's content lock or I/O. In + * this mode every bin member ends up either written (counted in the + * return value) or dropped, so callers derive the skip count as + * nbuf minus the result. */ -static int -FlushBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) +int +FlushBufferBin(const int *buf_ids, int nbuf, bool opportunistic, + WritebackContext *wb_context) { static char *bin_buf = NULL; @@ -4204,6 +4249,14 @@ FlushBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) ResourceOwnerEnlarge(CurrentResourceOwner); buf_state = LockBufHdr(bufHdr); + if (opportunistic && + (BUF_STATE_GET_REFCOUNT(buf_state) != 0 || + BUF_STATE_GET_USAGECOUNT(buf_state) != 0)) + { + /* a stale claim: the buffer became hot since it was queued */ + UnlockBufHdr(bufHdr, buf_state); + continue; + } if (!(buf_state & BM_VALID) || !(buf_state & BM_DIRTY)) { /* clean already: nothing to do */ @@ -4219,7 +4272,8 @@ FlushBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) * a fake unlogged LSN, so route it to the per-page path instead. */ UnlockBufHdr(bufHdr, buf_state); - fb_ids[nfallback++] = buf_ids[i]; + if (!opportunistic) + fb_ids[nfallback++] = buf_ids[i]; continue; } PinBuffer_Locked(bufHdr); @@ -4228,7 +4282,8 @@ FlushBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) LW_SHARED)) { UnpinBuffer(bufHdr); - fb_ids[nfallback++] = buf_ids[i]; + if (!opportunistic) + fb_ids[nfallback++] = buf_ids[i]; continue; } if (!StartBufferIO(bufHdr, false, true)) @@ -4236,11 +4291,13 @@ FlushBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) /* * Either somebody else's I/O is in flight (fall back per-page: * SyncOneBuffer may wait and rechecks dirtiness) or the buffer - * went clean; the fallback handles both. + * went clean; the fallback handles both. The opportunistic + * caller waits for neither and leaves the page to a later pass. */ LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); UnpinBuffer(bufHdr); - fb_ids[nfallback++] = buf_ids[i]; + if (!opportunistic) + fb_ids[nfallback++] = buf_ids[i]; continue; } @@ -4345,6 +4402,7 @@ FlushBufferBin(const int *buf_ids, int nbuf, WritebackContext *wb_context) } /* Phase 5: per-page fallback for the contended buffers, nothing held */ + Assert(!opportunistic || nfallback == 0); for (int i = 0; i < nfallback; i++) { if (SyncOneBuffer(fb_ids[i], false, wb_context) & BUF_WRITTEN) diff --git a/src/backend/storage/dwb/Makefile b/src/backend/storage/dwb/Makefile index fbbb1a3f93c8f..0b1cf707cd923 100644 --- a/src/backend/storage/dwb/Makefile +++ b/src/backend/storage/dwb/Makefile @@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global OBJS = \ dwb.o \ + dwb_cleaner.o \ dwb_ctl.o \ dwb_file.o \ dwb_recovery.o \ diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 642c26636a69c..272a0aa5a3f18 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -1082,14 +1082,18 @@ DWBGetBatchState(int batch_idx) */ /* - * The checkpointer's BufferSync and the bgwriter's flush rounds form the - * background stream; everything else — ordinary backend evictions above - * all — is the latency-critical class with first claim on FREE batches. + * The checkpointer's BufferSync, the bgwriter's flush rounds and the + * cleaner worker pool executing the bgwriter's bins form the background + * stream; everything else — ordinary backend evictions above all — is + * the latency-critical class with first claim on FREE batches. Cleaners + * are ordinary background workers, invisible to MyBackendType, hence the + * process-local flag. */ static int DWBWriterClass(void) { - if (MyBackendType == B_CHECKPOINTER || MyBackendType == B_BG_WRITER) + if (MyBackendType == B_CHECKPOINTER || MyBackendType == B_BG_WRITER || + DWBAmCleanerWorker) return DWB_WCLASS_BACKGROUND; return DWB_WCLASS_EVICTION; } diff --git a/src/backend/storage/dwb/dwb_cleaner.c b/src/backend/storage/dwb/dwb_cleaner.c new file mode 100644 index 0000000000000..0c592e2631a36 --- /dev/null +++ b/src/backend/storage/dwb/dwb_cleaner.c @@ -0,0 +1,342 @@ +/*------------------------------------------------------------------------- + * + * dwb_cleaner.c + * Cleaner worker pool of the short-lived double write buffer: takes + * the flush bins the bgwriter's LRU scan produces and executes them, + * so the scan's issue rate is no longer capped by one process + * serially waiting out a batch fdatasync per bin. + * + * The bgwriter stays the only LRU scanner and pacing estimator (the + * allocation counter of StrategySyncStart is consumed on read, so the + * estimator cannot be split across processes). What scales here is + * execution only: bins travel through a small shared-memory queue and + * any pool worker flushes them through FlushBufferBin's opportunistic + * mode. + * + * Queue entries are hints, not obligations. Every claim is + * reclassified under the buffer header lock right before the write; a + * buffer that was recycled, became hot, went clean or is busy with + * somebody's I/O is skipped, never waited on. Hence no draining on + * shutdown and no meaning to queue contents after a crash: whatever was + * queued is still dirty and the next checkpoint covers it. + * + * Backpressure is the enqueue failing (queue full, or its lock busy): + * the bgwriter then flushes the bin itself, which is exactly the + * pool-less behavior. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/storage/dwb/dwb_cleaner.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "miscadmin.h" +#include "pgstat.h" +#include "postmaster/bgworker.h" +#include "postmaster/interrupt.h" +#include "storage/buf_internals.h" +#include "storage/bufmgr.h" +#include "storage/condition_variable.h" +#include "storage/dwb.h" +#include "storage/ipc.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" +#include "tcop/tcopprot.h" +#include "utils/guc.h" +#include "utils/injection_point.h" +#include "utils/resowner.h" +#include "utils/wait_event.h" + +DWBCleanerCtl *DWBCleanerQueue = NULL; + +/* set for the lifetime of a cleaner worker; DWBWriterClass consults it */ +bool DWBAmCleanerWorker = false; + +/* + * Enough slack that a full pool finding the queue drained refills before + * the bgwriter's next round, small enough that entries stay fresh: stale + * claims are safe but wasted scan work. + */ +static int +DWBCleanerQueueCapacity(void) +{ + return Max(8, 2 * dwb_cleaner_workers); +} + +Size +DWBCleanerShmemSize(void) +{ + if (!DWBIsEnabled() || dwb_cleaner_workers == 0) + return 0; + + return add_size(offsetof(DWBCleanerCtl, bins), + mul_size(DWBCleanerQueueCapacity(), + sizeof(DWBCleanerBin))); +} + +void +DWBCleanerShmemInit(void) +{ + bool found; + + if (!DWBIsEnabled() || dwb_cleaner_workers == 0) + return; + + DWBCleanerQueue = (DWBCleanerCtl *) + ShmemInitStruct("DWB Cleaner Queue", DWBCleanerShmemSize(), &found); + + if (!found) + { + memset(DWBCleanerQueue, 0, DWBCleanerShmemSize()); + pg_atomic_init_u64(&DWBCleanerQueue->enqueued_pages, 0); + pg_atomic_init_u64(&DWBCleanerQueue->pool_written, 0); + pg_atomic_init_u64(&DWBCleanerQueue->pool_written_total, 0); + pg_atomic_init_u64(&DWBCleanerQueue->skipped_pages, 0); + pg_atomic_init_u64(&DWBCleanerQueue->self_flushes, 0); + ConditionVariableInit(&DWBCleanerQueue->cv_work); + DWBCleanerQueue->capacity = DWBCleanerQueueCapacity(); + } +} + +/* + * True when bins may be handed to the pool instead of flushed in place. + */ +bool +DWBCleanersActive(void) +{ + return DWBCleanerQueue != NULL; +} + +/* + * Hand one bin to the pool. Never waits: a busy queue lock or a full + * queue returns false and the caller decides what to do with the bin + * (the bgwriter flushes it itself and counts that as a self-flush). + */ +bool +DWBCleanerEnqueueBin(const int *buf_ids, int nbuf) +{ + DWBCleanerCtl *ctl = DWBCleanerQueue; + DWBCleanerBin *bin; + + Assert(ctl != NULL); + Assert(nbuf > 0 && nbuf <= DWB_FLUSH_BIN_MAX); + + if (!LWLockConditionalAcquire(DWBCleanerQueueLock, LW_EXCLUSIVE)) + return false; + if (ctl->nqueued == ctl->capacity) + { + LWLockRelease(DWBCleanerQueueLock); + return false; + } + + bin = &ctl->bins[(ctl->head + ctl->nqueued) % ctl->capacity]; + bin->nbuf = nbuf; + memcpy(bin->buf_ids, buf_ids, nbuf * sizeof(int)); + ctl->nqueued++; + LWLockRelease(DWBCleanerQueueLock); + + pg_atomic_fetch_add_u64(&ctl->enqueued_pages, nbuf); + ConditionVariableSignal(&ctl->cv_work); + return true; +} + +/* + * Take the oldest bin, if any. A local copy is returned so the queue + * lock is never held across the flush. + */ +static bool +DWBCleanerDequeueBin(DWBCleanerBin *bin) +{ + DWBCleanerCtl *ctl = DWBCleanerQueue; + bool got = false; + + LWLockAcquire(DWBCleanerQueueLock, LW_EXCLUSIVE); + if (ctl->nqueued > 0) + { + *bin = ctl->bins[ctl->head]; + ctl->head = (ctl->head + 1) % ctl->capacity; + ctl->nqueued--; + got = true; + } + LWLockRelease(DWBCleanerQueueLock); + return got; +} + +/* + * The bgwriter folds the pool's completed writes into + * PendingBgWriterStats.buf_written_clean once per round, keeping + * pg_stat_bgwriter's counter "pages written by LRU cleaning" no matter + * which process executed the write. + */ +uint64 +DWBCleanerFetchPoolWritten(void) +{ + if (!DWBCleanersActive()) + return 0; + return pg_atomic_exchange_u64(&DWBCleanerQueue->pool_written, 0); +} + +/* + * The bgwriter reports a bin it had to flush itself after a failed + * enqueue. Counted at the flush, not inside the failed enqueue: a + * refused test claim flushes nothing. + */ +void +DWBCleanerCountSelfFlush(void) +{ + pg_atomic_fetch_add_u64(&DWBCleanerQueue->self_flushes, 1); +} + +/* + * Register dwb_cleaner_workers static background workers. Called from + * PostmasterMain right after the retire pool registration, before + * extensions get a chance at the worker slots. + */ +void +DWBCleanerWorkersRegister(void) +{ + BackgroundWorker bgw; + int free_slots; + + if (!DWBIsEnabled() || dwb_cleaner_workers == 0) + return; + + /* + * RegisterBackgroundWorker only LOGs on overflow, so check the slots + * actually left and fail loudly: a silently missing cleaner would ship a + * smaller pool than the operator configured. + */ + free_slots = max_worker_processes - GetNumRegisteredBackgroundWorkers(); + if (dwb_cleaner_workers > free_slots) + ereport(FATAL, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"dwb_cleaner_workers\" (%d) needs more \"max_worker_processes\" slots than remain free (%d)", + dwb_cleaner_workers, free_slots), + errhint("Increase \"max_worker_processes\" or decrease \"dwb_cleaner_workers\"."))); + + for (int i = 0; i < dwb_cleaner_workers; i++) + { + memset(&bgw, 0, sizeof(bgw)); + + /* + * The database-less connection gives the worker a pg_stat_activity + * entry; it forces BgWorkerStart_ConsistentState, which is fine: the + * bgwriter that feeds the queue starts even later, and until then the + * queue simply stays empty. + */ + bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | + BGWORKER_BACKEND_DATABASE_CONNECTION; + bgw.bgw_start_time = BgWorkerStart_ConsistentState; + snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres"); + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DWBCleanerWorkerMain"); + snprintf(bgw.bgw_name, BGW_MAXLEN, "dwb cleaner %d", i); + snprintf(bgw.bgw_type, BGW_MAXLEN, "dwb cleaner"); + bgw.bgw_restart_time = 1; + bgw.bgw_notify_pid = 0; + bgw.bgw_main_arg = Int32GetDatum(i); + + RegisterBackgroundWorker(&bgw); + } +} + +/* + * Main loop: flush queued bins, sleep when the queue is empty. + */ +void +DWBCleanerWorkerMain(Datum main_arg) +{ + WritebackContext wb_context; + + pqsignal(SIGHUP, SignalHandlerForConfigReload); + + /* + * die, not a shutdown flag: a worker that exits with code 0 is + * unregistered for good, so one stray SIGTERM would permanently shrink + * the pool. The FATAL exit restarts after bgw_restart_time outside a + * postmaster shutdown and is simply the end during one. + */ + pqsignal(SIGTERM, die); + BackgroundWorkerUnblockSignals(); + + /* no database, just shared state and pg_stat_activity visibility */ + BackgroundWorkerInitializeConnection(NULL, NULL, 0); + + /* + * FlushBufferBin pins buffers and registers its buffer I/O with + * CurrentResourceOwner — that registration is what repairs an + * interrupted data-file write from the batch copy if an ERROR throws the + * worker out mid-bin. The aux-process owner provides both the owner and + * its shmem-exit release. + */ + CreateAuxProcessResourceOwner(); + + DWBAmCleanerWorker = true; + WritebackContextInit(&wb_context, &bgwriter_flush_after); + + for (;;) + { + DWBCleanerBin bin; + int written; + + /* the CFI is what turns a pending die() into the FATAL exit */ + CHECK_FOR_INTERRUPTS(); + + if (ConfigReloadPending) + { + ConfigReloadPending = false; + ProcessConfigFile(PGC_SIGHUP); + } + + INJECTION_POINT("dwb-cleaner-loop", NULL); + + if (!DWBCleanerDequeueBin(&bin)) + { + /* + * Sleep without losing a wakeup: get onto the wait list first, + * then recheck under the queue lock, then sleep. A signal sent + * after the recheck is kept by the prepared state; a bin enqueued + * before it is seen by the recheck. The sleep itself checks for + * interrupts, so a pending die() cuts it short. + */ + ConditionVariablePrepareToSleep(&DWBCleanerQueue->cv_work); + if (!DWBCleanerDequeueBin(&bin)) + { + IssuePendingWritebacks(&wb_context, IOCONTEXT_NORMAL); + + /* + * Flush I/O statistics while idle: nothing else in this loop + * reports them, and the worker's pg_stat_io rows are how an + * operator sees the pool actually writing. Forced, because a + * deferred report would sit on local counters through the + * whole open-ended sleep that follows. The injection point + * lets a test park the worker right after the report; a + * parked worker switches its prepared condition-variable + * sleep to the injection one, which the sleep below repairs + * by re-preparing and returning for another loop. + */ + pgstat_report_stat(true); + INJECTION_POINT("dwb-cleaner-reported", NULL); + ConditionVariableSleep(&DWBCleanerQueue->cv_work, + WAIT_EVENT_DWB_CLEANER_MAIN); + continue; + } + } + + /* off the wait list while flushing (no-op if never prepared) */ + ConditionVariableCancelSleep(); + + /* + * In opportunistic mode every bin member ends up either written or + * skipped, so the skip count needs no extra plumbing. + */ + written = FlushBufferBin(bin.buf_ids, bin.nbuf, true, &wb_context); + pg_atomic_fetch_add_u64(&DWBCleanerQueue->pool_written, written); + pg_atomic_fetch_add_u64(&DWBCleanerQueue->pool_written_total, written); + pg_atomic_fetch_add_u64(&DWBCleanerQueue->skipped_pages, + bin.nbuf - written); + } +} diff --git a/src/backend/storage/dwb/dwb_ctl.c b/src/backend/storage/dwb/dwb_ctl.c index 26820050e1cd2..5edd13ef90114 100644 --- a/src/backend/storage/dwb/dwb_ctl.c +++ b/src/backend/storage/dwb/dwb_ctl.c @@ -25,6 +25,7 @@ int dwb_num_batches = 64; int dwb_batch_pages = 64; int dwb_max_segments = 4096; int dwb_retire_workers = 1; +int dwb_cleaner_workers = 0; int dwb_retire_sync_method = DWB_RETIRE_SYNC_METHOD_DEFAULT; int dwb_batch_timeout_ms = 10; int dwb_retire_interval_ms = 50; @@ -72,6 +73,7 @@ DWBShmemSize(void) size = add_size(DWBCtlSize(), DWBStagingSize()); size = add_size(size, hash_estimate_size(dwb_max_segments, DWBSegEntrySize())); + size = add_size(size, DWBCleanerShmemSize()); return size; } @@ -144,4 +146,6 @@ DWBShmemInit(void) &info, HASH_ELEM | HASH_BLOBS | HASH_FIXED_SIZE); } + + DWBCleanerShmemInit(); } diff --git a/src/backend/storage/dwb/meson.build b/src/backend/storage/dwb/meson.build index cc84a0fa8316b..d59954d5ce75a 100644 --- a/src/backend/storage/dwb/meson.build +++ b/src/backend/storage/dwb/meson.build @@ -2,6 +2,7 @@ backend_sources += files( 'dwb.c', + 'dwb_cleaner.c', 'dwb_ctl.c', 'dwb_file.c', 'dwb_recovery.c', diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 289b389556a7e..c9ab8ca178f09 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -57,6 +57,7 @@ BGWRITER_HIBERNATE "Waiting in background writer process, hibernating." BGWRITER_MAIN "Waiting in main loop of background writer process." CHECKPOINTER_MAIN "Waiting in main loop of checkpointer process." CHECKPOINTER_SHUTDOWN "Waiting for checkpointer process to be terminated." +DWB_CLEANER_MAIN "Waiting in main loop of a double write buffer cleaner worker." DWB_RETIRE_MAIN "Waiting in main loop of a double write buffer retire worker." IO_WORKER_MAIN "Waiting in main loop of IO Worker process." LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process." @@ -368,6 +369,7 @@ DWBRingOpen "Waiting to open a new double write buffer batch." DWBSegHash "Waiting to read or update the double write buffer segment hash table." DWBSelfSweep "Waiting to run the double write buffer self-help retirement sweep." DWBSyncfsRound "Waiting to run a wholesale double write buffer retirement round." +DWBCleanerQueue "Waiting to access the double write buffer cleaner work queue." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index cc5b410b8e1a2..d0758786c99b4 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -2236,6 +2236,19 @@ struct config_int ConfigureNamesInt[] = 1, 0, 32, NULL, NULL, NULL }, + { + {"dwb_cleaner_workers", PGC_POSTMASTER, WAL_SETTINGS, + gettext_noop("Number of double write buffer cleaner worker processes."), + gettext_noop("The pool executes the flush bins the background " + "writer's LRU scan produces, so the scan's issue rate " + "is not capped by one process. The workers consume " + "\"max_worker_processes\" slots. 0 disables the pool " + "and the background writer flushes its bins itself.") + }, + &dwb_cleaner_workers, + 0, 0, 64, + NULL, NULL, NULL + }, { {"dwb_batch_timeout_ms", PGC_SIGHUP, WAL_SETTINGS, gettext_noop("Maximum time an open double write buffer batch may wait before being sealed."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index c7ab2b3842b5c..1875b61109a64 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -257,6 +257,9 @@ # (change requires restart) #dwb_retire_workers = 1 # retire worker processes # (change requires restart) +#dwb_cleaner_workers = 0 # cleaner worker processes executing the + # background writer's flush bins + # (change requires restart) #dwb_retire_sync_method = syncfs # syncfs where supported (Linux; the # default there), fsync elsewhere #dwb_batch_timeout_ms = 10ms # force-seal an open batch after this time diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 0dec7d93b3b27..57f3a8587784a 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -434,6 +434,14 @@ extern void IssuePendingWritebacks(WritebackContext *wb_context, IOContext io_co extern void ScheduleBufferTagForWriteback(WritebackContext *wb_context, IOContext io_context, BufferTag *tag); +/* + * The vectored DWB flush of one bin of buffers; exported for the cleaner + * worker pool (dwb_cleaner.c), which executes the bgwriter's queued bins + * in opportunistic mode. + */ +extern int FlushBufferBin(const int *buf_ids, int nbuf, bool opportunistic, + WritebackContext *wb_context); + /* solely to make it easier to write tests */ extern bool StartBufferIO(BufferDesc *buf, bool forInput, bool nowait); extern void TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint32 set_flag_bits, diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 1e2a8f9ff9a85..9c61af510b7ef 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -68,6 +68,7 @@ extern PGDLLIMPORT int dwb_num_batches; extern PGDLLIMPORT int dwb_batch_pages; extern PGDLLIMPORT int dwb_max_segments; extern PGDLLIMPORT int dwb_retire_workers; +extern PGDLLIMPORT int dwb_cleaner_workers; extern PGDLLIMPORT int dwb_retire_sync_method; extern PGDLLIMPORT int dwb_batch_timeout_ms; extern PGDLLIMPORT int dwb_retire_interval_ms; @@ -95,6 +96,18 @@ extern PGDLLIMPORT int dwb_on_stall; #define DWB_WCLASS_EVICTION 0 #define DWB_WCLASS_BACKGROUND 1 +/* + * Bin size cap for the vectored background flush (FlushBufferBin, used by + * the checkpointer's BufferSync, the bgwriter's LRU scan and the cleaner + * worker pool): the flush holds a pin, a shared content lock and + * BM_IO_IN_PROGRESS per bin member at once, so the cap must leave + * MAX_SIMUL_LWLOCKS (200) plenty of headroom. 64 matches the default + * dwb_batch_pages; larger batch_pages settings seal their batches at + * bin-sized fills. Shared here because the cleaner work queue stores + * bins of this size. + */ +#define DWB_FLUSH_BIN_MAX 64 + /* * Why a batch was sealed. Purely diagnostic: per-class seal and page * counters in DWCtl attribute batch turnover to its trigger, which is how @@ -423,6 +436,49 @@ typedef struct DWBSlotRef uint64 batch_id; } DWBSlotRef; +/* + * Work queue between the bgwriter's LRU scan and the cleaner worker pool + * (dwb_cleaner.c). An entry is one flush bin: buffer ids the scan + * classified as cold dirty candidates. Entries are hints, not + * obligations — every claim is reclassified under the buffer header lock + * right before the write (FlushBufferBin's opportunistic mode), so a + * stale entry is skipped, never wrongly written, and the queue needs no + * draining on shutdown: whatever it held stays dirty and is covered by + * the next checkpoint. + * + * In error-free operation every accepted page ends up counted as either + * written or skipped, so enqueued_pages = pool_written_total + + * skipped_pages once the queue is empty. A worker error mid-bin + * abandons the bin's remainder — those pages stay dirty and are simply + * rescanned later, but they leave the counters short of the identity. + */ +typedef struct DWBCleanerBin +{ + int nbuf; + int buf_ids[DWB_FLUSH_BIN_MAX]; +} DWBCleanerBin; + +typedef struct DWBCleanerCtl +{ + /* counters are monotonic except pool_written, which bgwriter drains */ + pg_atomic_uint64 enqueued_pages; /* pages ever accepted into the queue */ + pg_atomic_uint64 pool_written; /* pages written by cleaners since the + * bgwriter last folded them into + * buf_written_clean */ + pg_atomic_uint64 pool_written_total; /* same, never reset (tests, + * diagnostics) */ + pg_atomic_uint64 skipped_pages; /* stale claims dropped by + * reclassification */ + pg_atomic_uint64 self_flushes; /* bins the bgwriter flushed itself + * because the queue was full or busy */ + ConditionVariable cv_work; /* one targeted signal per enqueued bin */ + int capacity; + /* head/nqueued and the bins are protected by DWBCleanerQueueLock */ + int head; + int nqueued; + DWBCleanerBin bins[FLEXIBLE_ARRAY_MEMBER]; /* capacity entries */ +} DWBCleanerCtl; + extern PGDLLIMPORT DWCtl *DWBCtl; extern PGDLLIMPORT char *DWBStagingBase; extern PGDLLIMPORT HTAB *DWSegmentHash; @@ -463,6 +519,18 @@ extern int DWBRetireAllSync(void); extern void DWBRetireWorkersRegister(void); pg_noreturn extern void DWBRetireWorkerMain(Datum main_arg); +/* dwb_cleaner.c — bgwriter bin queue and the cleaner worker pool */ +extern PGDLLIMPORT DWBCleanerCtl *DWBCleanerQueue; +extern PGDLLIMPORT bool DWBAmCleanerWorker; +extern Size DWBCleanerShmemSize(void); +extern void DWBCleanerShmemInit(void); +extern bool DWBCleanersActive(void); +extern bool DWBCleanerEnqueueBin(const int *buf_ids, int nbuf); +extern uint64 DWBCleanerFetchPoolWritten(void); +extern void DWBCleanerCountSelfFlush(void); +extern void DWBCleanerWorkersRegister(void); +pg_noreturn extern void DWBCleanerWorkerMain(Datum main_arg); + /* dwb_file.c */ extern void DWBCreateRing(void); extern void DWBBatchFilePath(char *path, int batch_idx); diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 74006de5f1e67..9f331951e255c 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -88,3 +88,4 @@ PG_LWLOCK(54, DWBRingOpen) PG_LWLOCK(55, DWBSegHash) PG_LWLOCK(56, DWBSelfSweep) PG_LWLOCK(57, DWBSyncfsRound) +PG_LWLOCK(58, DWBCleanerQueue) diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index 773fb68d004b7..880bcc84db591 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -54,6 +54,7 @@ tests += { 't/015_vectored_flush.pl', 't/016_bgwriter_bin.pl', 't/017_syncfs_retire.pl', + 't/018_cleaners.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/018_cleaners.pl b/src/test/modules/test_dwb/t/018_cleaners.pl new file mode 100644 index 0000000000000..71d3d3fbce61e --- /dev/null +++ b/src/test/modules/test_dwb/t/018_cleaners.pl @@ -0,0 +1,407 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# The cleaner worker pool: the bgwriter's LRU scan hands its flush bins +# to a shared-memory queue and dwb_cleaner_workers background workers +# execute them. Queue entries are hints — every claim is reclassified +# right before the write — so the scenarios here drive the queue with +# deterministic one-page claims: a cold dirty page is written, a pinned +# page and an already-clean page are skipped, a full queue makes the +# bgwriter flush bins itself. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_cleaners'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 1 +dwb_cleaner_workers = 2 +shared_buffers = 2MB +bgwriter_delay = 10ms +bgwriter_lru_maxpages = 1000 +bgwriter_lru_multiplier = 10 +checkpoint_timeout = 1h +autovacuum = off +)); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); + +$node->poll_query_until('postgres', + "SELECT count(*) = 2 FROM pg_stat_activity WHERE backend_type = 'dwb cleaner'" +) or die 'timed out waiting for the cleaner workers to start'; +pass('two cleaner workers are visible in pg_stat_activity'); + +sub counter +{ + my ($name) = @_; + return $node->safe_psql('postgres', + "SELECT $name FROM test_dwb_cleaner_counters()"); +} + +# A quiet baseline: nothing dirty, the bgwriter has nothing to scan. +$node->safe_psql('postgres', 'CHECKPOINT'); + +# --- a stale-claim queue entry naming a cold dirty page is written ------- + +# enqueue_block cools the page as it queues the claim (production pages +# go cold when the clock hand sweeps past); the page is dirty and nobody +# holds it, so this claim must be written. The system is quiet after +# the checkpoint, so background-class DWB pages can only come from the +# cleaner executing this claim — pinned by the class-page growth with +# the self-flush counter standing still (nothing for the bgwriter to +# flush itself). +my $written_before = counter('written'); +my $self_before = counter('self_flushes'); +my $bg_pages_before = $node->safe_psql('postgres', + "SELECT sum(pages) FROM test_dwb_seal_stats() WHERE wclass = 'background'" +); +$node->safe_psql( + 'postgres', q( + CREATE TABLE t_cold (id int); + INSERT INTO t_cold VALUES (1); +)); +is( $node->safe_psql( + 'postgres', "SELECT test_dwb_enqueue_block('t_cold', 0)"), + 't', + 'the queue accepted a claim on a cold dirty page'); +$node->poll_query_until('postgres', + "SELECT written > $written_before FROM test_dwb_cleaner_counters()") + or die 'timed out waiting for a cleaner to write the cold dirty page'; +pass('a cleaner wrote the queued cold dirty page'); +$node->poll_query_until( + 'postgres', qq( + SELECT sum(pages) > $bg_pages_before FROM test_dwb_seal_stats() + WHERE wclass = 'background' +)) or die 'timed out waiting for the background-class DWB batch'; +is(counter('self_flushes'), $self_before, + 'the background-class write was the cleaner, not a bgwriter self-flush'); + +# --- a claim on a page that became hot is skipped ------------------------ + +# The pin is held across statements by an open transaction in a second +# session; the enqueue then races nothing — the claim is stale from the +# start and the reclassification must drop it without waiting. +my $skipped_before = counter('skipped'); +$node->safe_psql( + 'postgres', q( + CREATE TABLE t_hot (id int); + INSERT INTO t_hot VALUES (1); +)); +my $pinner = $node->background_psql('postgres'); +$pinner->query_safe('BEGIN'); +$pinner->query_safe("SELECT test_dwb_pin_block('t_hot', 0)"); + +is( $node->safe_psql('postgres', "SELECT test_dwb_enqueue_block('t_hot', 0)"), + 't', + 'the queue accepted a claim on a pinned page'); +$node->poll_query_until('postgres', + "SELECT skipped > $skipped_before FROM test_dwb_cleaner_counters()") + or die 'timed out waiting for the pinned page claim to be skipped'; +pass('the claim on the pinned page was skipped, not written'); + +$pinner->query_safe('SELECT test_dwb_unpin_block()'); +$pinner->query_safe('COMMIT'); + +# A rollback releases the pin through the resource owner; the helpers +# must notice and be usable again in the next transaction. +$pinner->query_safe('BEGIN'); +$pinner->query_safe("SELECT test_dwb_pin_block('t_hot', 0)"); +$pinner->query_safe('ROLLBACK'); +$pinner->query_safe('BEGIN'); +$pinner->query_safe("SELECT test_dwb_pin_block('t_hot', 0)"); +$pinner->query_safe('SELECT test_dwb_unpin_block()'); +$pinner->query_safe('COMMIT'); +$pinner->quit; +pass('the pin helpers survive a rollback and pin again'); + +# --- a duplicate claim finds the page clean and is skipped --------------- + +# t_cold's page was written by the first scenario; a second claim on the +# same block must resolve as a skip (clean already), never a rewrite. +$skipped_before = counter('skipped'); +is( $node->safe_psql( + 'postgres', "SELECT test_dwb_enqueue_block('t_cold', 0)"), + 't', + 'the queue accepted a duplicate claim'); +$node->poll_query_until('postgres', + "SELECT skipped > $skipped_before FROM test_dwb_cleaner_counters()") + or die 'timed out waiting for the duplicate claim to be skipped'; +pass('the duplicate claim on the clean page was skipped'); + +# --- the production path: scan feeds the queue, the pool executes -------- + +# A workload well past shared_buffers leaves plenty of cold dirty pages +# behind; the bgwriter's scan bins them into the queue and the pool +# must execute them (the class and statistics attribution have their +# own quiet-window scenarios). +my $organic_before = counter('written'); +$node->safe_psql( + 'postgres', q( + CREATE TABLE t_organic AS + SELECT g AS id, repeat('o', 300) AS filler + FROM generate_series(1, 20000) g; + UPDATE t_organic SET filler = repeat('p', 300) WHERE id % 5 = 0; +)); +$node->poll_query_until('postgres', + "SELECT written >= $organic_before + 16 FROM test_dwb_cleaner_counters()") + or die 'timed out waiting for the pool to execute scan-produced bins'; +pass('the pool executed at least one full scan-produced bin'); +# pg_stat_io aggregates by process type, so the pool shows up under +# 'background worker' (bgw_type granularity exists only in +# pg_stat_activity); nothing else of that type writes DWB batches here. +$node->poll_query_until( + 'postgres', q( + SELECT sum(writes) > 0 FROM pg_stat_io + WHERE object = 'dwb' AND backend_type = 'background worker' +)) or die 'timed out waiting for the cleaner pg_stat_io dwb rows'; +pass('the cleaners show their DWB writes in pg_stat_io'); + +# --- a full queue makes the enqueue fail and the bgwriter clean solo ----- + +SKIP: +{ + skip 'injection points not supported by this build', 4 + unless defined $ENV{enable_injection_points} + && $ENV{enable_injection_points} eq 'yes'; + + $node->safe_psql('postgres', 'CREATE EXTENSION injection_points'); + $node->safe_psql('postgres', + "SELECT injection_points_attach('dwb-cleaner-loop', 'wait')"); + + # Idle workers sleep on the queue's condition variable and only pass + # the loop top — where the point sits — when woken; one targeted + # signal per enqueued claim wakes them one by one. The two wake-up + # claims park both workers BEFORE any dequeue, so they stay queued. + $node->safe_psql('postgres', + "SELECT test_dwb_enqueue_block('t_cold', 0)"); + $node->safe_psql('postgres', + "SELECT test_dwb_enqueue_block('t_cold', 0)"); + $node->poll_query_until( + 'postgres', q( + SELECT count(*) FILTER (WHERE wait_event = 'dwb-cleaner-loop') = 2 + FROM pg_stat_activity WHERE backend_type = 'dwb cleaner' + )) or die 'timed out waiting for both cleaners to park at the point'; + pass('both cleaners parked at the injection point'); + + # Capacity is Max(8, 2 * workers) = 8 bins; nine claims into a parked + # queue must overflow it (the bgwriter may race a bin or two in, so + # the accepted count is bounded, not exact). A refused test claim + # flushes nothing and must NOT move the self-flush counter — that is + # the bgwriter's own bookkeeping, checked right below. + $node->safe_psql( + 'postgres', q( + CREATE TABLE t_fill AS + SELECT g AS id, repeat('x', 800) AS filler + FROM generate_series(1, 80) g; + )); + my $accepted = 0; + for my $blk (0 .. 8) + { + $accepted++ + if $node->safe_psql('postgres', + "SELECT test_dwb_enqueue_block('t_fill', $blk)") eq 't'; + } + cmp_ok($accepted, '<=', 8, + 'the queue turned the overflow claims away at its capacity'); + + # With the pool parked and the queue full, the bgwriter keeps cleaning + # alone: its enqueues fail and it flushes the bins itself. + my $self_before = counter('self_flushes'); + $node->safe_psql( + 'postgres', q( + CREATE TABLE t_solo AS + SELECT g AS id, repeat('y', 800) AS filler + FROM generate_series(1, 2000) g; + UPDATE t_solo SET filler = repeat('z', 800) WHERE id % 3 = 0; + )); + $node->poll_query_until('postgres', + "SELECT self_flushes > $self_before FROM test_dwb_cleaner_counters()") + or die 'timed out waiting for the bgwriter to self-flush bins'; + pass('the bgwriter self-flushed bins while the pool was parked'); + + # Detach BEFORE waking: a woken worker loops back to the point, and + # with it still attached it would park again with no wakeup left. + # Each wakeup releases ONE waiter (the first waiter slot matching the + # point name), so keep nudging until both workers are off the point — + # a worker left parked here would sleep on the injection DSM forever + # and its eventual FATAL exit would touch the detached segment. + $node->safe_psql('postgres', + "SELECT injection_points_detach('dwb-cleaner-loop')"); + my $deadline = time() + 30; + while (time() < $deadline) + { + last + if $node->safe_psql('postgres', + "SELECT count(*) FROM pg_stat_activity WHERE wait_event = 'dwb-cleaner-loop'" + ) == 0; + $node->psql('postgres', + "SELECT injection_points_wakeup('dwb-cleaner-loop')"); + } + is( $node->safe_psql( + 'postgres', + "SELECT count(*) FROM pg_stat_activity WHERE wait_event = 'dwb-cleaner-loop'" + ), + '0', + 'both cleaners left the injection point'); +} + +# --- the queue drains and the counters reconcile ------------------------- + +# Every accepted claim ends as written or skipped, nothing else: once the +# queue is empty, enqueued = written + skipped exactly. +$node->poll_query_until( + 'postgres', q( + SELECT queued = 0 AND enqueued = written + skipped + FROM test_dwb_cleaner_counters() +)) or die 'timed out waiting for the queue to drain and reconcile'; +pass('the drained queue reconciles: enqueued = written + skipped'); + +# --- one quiet claim moves buffers_clean and republishes pg_stat_io ------ + +# With the queue drained and the system quiet, let buffers_clean settle +# (pending folds of the workload above trickle in with the bgwriter's +# reporting), then drive exactly one pool write and pin the attribution: +# buffers_clean grows while the self-flush counter stands still, and the +# worker's pg_stat_io row reflects the new write too (that the report is +# forced, not merely allowed by the stats interval, has its own +# injection-point scenario below). +my ($bclean_base, $bclean_prev) = (0, -1); +my $deadline = time() + 60; +while (time() < $deadline) +{ + $bclean_base = $node->safe_psql('postgres', + 'SELECT buffers_clean FROM pg_stat_bgwriter'); + last if $bclean_base == $bclean_prev; + $bclean_prev = $bclean_base; + sleep 1; +} +my $self_base = counter('self_flushes'); +my $io_base = $node->safe_psql('postgres', + "SELECT sum(writes) FROM pg_stat_io WHERE object = 'dwb' AND backend_type = 'background worker'" +); +$node->safe_psql( + 'postgres', q( + CREATE TABLE t_fold (id int); + INSERT INTO t_fold VALUES (1); +)); +is( $node->safe_psql( + 'postgres', "SELECT test_dwb_enqueue_block('t_fold', 0)"), + 't', + 'the queue accepted the attribution claim'); +$node->poll_query_until( + 'postgres', qq( + SELECT buffers_clean > $bclean_base FROM pg_stat_bgwriter +)) or die 'timed out waiting for the pool write to reach buffers_clean'; +is(counter('self_flushes'), $self_base, + 'the buffers_clean growth came through the pool fold alone'); +$node->poll_query_until( + 'postgres', qq( + SELECT sum(writes) > $io_base FROM pg_stat_io + WHERE object = 'dwb' AND backend_type = 'background worker' +)) or die 'timed out waiting for the pg_stat_io row to grow again'; +pass('the attribution write reached pg_stat_io as well'); + +# --- a terminated worker is restarted, the pool keeps its size ----------- + +# The workers exit FATAL on SIGTERM (exit code 1): a zero exit would +# unregister the worker for good and one stray terminate would shrink +# the pool permanently. +my $victim = $node->safe_psql('postgres', + "SELECT pid FROM pg_stat_activity WHERE backend_type = 'dwb cleaner' LIMIT 1" +); +$node->safe_psql('postgres', "SELECT pg_terminate_backend($victim)"); +$node->poll_query_until( + 'postgres', qq( + SELECT count(*) = 2 AND count(*) FILTER (WHERE pid = $victim) = 0 + FROM pg_stat_activity WHERE backend_type = 'dwb cleaner' +)) or die 'timed out waiting for the terminated cleaner to be replaced'; +pass('a terminated cleaner was restarted and the pool is back to size'); + +# --- crash recovery on top of pool-cleaned data -------------------------- + +$node->stop('immediate'); +$node->start; +is($node->safe_psql('postgres', 'SELECT count(*) FROM t_cold'), + '1', 'data intact after crash recovery'); + +# --- the idle-time report is forced, not deferred ------------------------ + +# The dwb-cleaner-reported point sits right AFTER pgstat_report_stat: +# a parked worker has published everything it wrote so far. The next +# claim is executed milliseconds after the wakeup, so the worker's next +# report attempt lands well inside PGSTAT_MIN_INTERVAL of its previous +# one — a non-forced report would be suppressed there, and since the +# worker then sleeps indefinitely, the write would never surface. The +# delta becoming visible is therefore the forced report and nothing +# else. One worker, so no second cleaner can publish the delta on its +# own schedule. +SKIP: +{ + skip 'injection points not supported by this build', 2 + unless defined $ENV{enable_injection_points} + && $ENV{enable_injection_points} eq 'yes'; + + $node->stop; + $node->append_conf('postgresql.conf', 'dwb_cleaner_workers = 1'); + $node->start; + + $node->safe_psql( + 'postgres', q( + CREATE TABLE t_pub (id int); + INSERT INTO t_pub VALUES (1); + )); + $node->safe_psql('postgres', + "SELECT injection_points_attach('dwb-cleaner-reported', 'wait')"); + + # The waker claim: the sleeping worker only reaches the point after + # processing something and going idle again. + $node->safe_psql('postgres', "SELECT test_dwb_enqueue_block('t_pub', 0)"); + $node->wait_for_event('dwb cleaner', 'dwb-cleaner-reported'); + pass('the worker parked right after publishing its statistics'); + + my $io_pub = $node->safe_psql('postgres', + "SELECT sum(writes) FROM pg_stat_io WHERE object = 'dwb' AND backend_type = 'background worker'" + ); + $node->safe_psql( + 'postgres', q( + INSERT INTO t_pub VALUES (2); + )); + $node->safe_psql('postgres', "SELECT test_dwb_enqueue_block('t_pub', 0)"); + + $node->safe_psql('postgres', + "SELECT injection_points_detach('dwb-cleaner-reported')"); + $node->safe_psql('postgres', + "SELECT injection_points_wakeup('dwb-cleaner-reported')"); + + $node->poll_query_until( + 'postgres', qq( + SELECT sum(writes) > $io_pub FROM pg_stat_io + WHERE object = 'dwb' AND backend_type = 'background worker' + )) or die 'timed out waiting for the forced back-to-back publication'; + pass('the report published a delta inside the minimum stats interval'); +} + +# --- a pool larger than the free worker slots refuses to start ----------- + +$node->stop; +$node->append_conf( + 'postgresql.conf', qq( +max_worker_processes = 2 +dwb_cleaner_workers = 8 +)); +my $ret = $node->start(fail_ok => 1); +is($ret, 0, 'start with an oversized cleaner pool fails'); +ok( $node->log_contains( + qr/"dwb_cleaner_workers" \(8\) needs more "max_worker_processes" slots/ + ), + 'the refusal names the pool size and the slot shortage'); + +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index 16a801c2e1709..b9502e2aa20a1 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -104,3 +104,21 @@ CREATE FUNCTION test_dwb_craft_batch(batch_idx int, batch_id int8, CREATE FUNCTION test_dwb_set_control_min_version(min_version int) RETURNS void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_cleaner_counters( + OUT enqueued bigint, OUT written bigint, OUT skipped bigint, + OUT self_flushes bigint, OUT queued int) + RETURNS record STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_pin_block(rel regclass, blkno int) + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_unpin_block() + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_enqueue_block(rel regclass, blkno int) + RETURNS bool STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index b3e87d81f575d..f11114bb7a242 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -19,19 +19,27 @@ #include #include +#include "access/htup_details.h" +#include "access/relation.h" +#include "access/xact.h" #include "catalog/pg_tablespace_d.h" #include "common/relpath.h" #include "fmgr.h" #include "funcapi.h" #include "miscadmin.h" +#include "storage/buf_internals.h" +#include "storage/bufmgr.h" #include "storage/bufpage.h" #include "storage/checksum.h" #include "storage/dwb.h" #include "storage/fd.h" +#include "storage/lwlock.h" #include "storage/smgr.h" #include "storage/sync.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" +#include "utils/rel.h" +#include "utils/resowner.h" #include "utils/timestamp.h" #include "varatt.h" @@ -1076,3 +1084,188 @@ test_dwb_set_control_min_version(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } + +/* ---------------------------------------------------------------- + * cleaner worker pool helpers + * ---------------------------------------------------------------- + */ + +static void +check_cleaners_enabled(void) +{ + check_dwb_enabled(); + if (!DWBCleanersActive()) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("the cleaner pool is not configured"), + errhint("Set \"dwb_cleaner_workers\" above 0."))); +} + +/* + * Counters of the cleaner work queue: enqueued/written/skipped pages, + * bgwriter self-flushed bins, bins currently queued. The written count + * is the never-reset total (the drainable one feeds pg_stat_bgwriter). + */ +PG_FUNCTION_INFO_V1(test_dwb_cleaner_counters); +Datum +test_dwb_cleaner_counters(PG_FUNCTION_ARGS) +{ + TupleDesc tupdesc; + Datum values[5]; + bool nulls[5] = {0}; + int queued; + + check_cleaners_enabled(); + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + LWLockAcquire(DWBCleanerQueueLock, LW_SHARED); + queued = DWBCleanerQueue->nqueued; + LWLockRelease(DWBCleanerQueueLock); + + values[0] = Int64GetDatum( + (int64) pg_atomic_read_u64(&DWBCleanerQueue->enqueued_pages)); + values[1] = Int64GetDatum( + (int64) pg_atomic_read_u64(&DWBCleanerQueue->pool_written_total)); + values[2] = Int64GetDatum( + (int64) pg_atomic_read_u64(&DWBCleanerQueue->skipped_pages)); + values[3] = Int64GetDatum( + (int64) pg_atomic_read_u64(&DWBCleanerQueue->self_flushes)); + values[4] = Int32GetDatum(queued); + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + +/* + * Resolve (relation, block) to the buffer currently holding it. The + * transient pin is dropped before returning; the id is a hint exactly + * like a queued bin entry. + */ +static int +lookup_block_buf_id(Oid relid, BlockNumber blkno) +{ + Relation rel; + Buffer buf; + int buf_id; + + rel = relation_open(relid, AccessShareLock); + buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, NULL); + buf_id = buf - 1; + ReleaseBuffer(buf); + /* keep the relation lock till end of transaction */ + relation_close(rel, NoLock); + return buf_id; +} + +/* + * Pin one block for the rest of the current transaction (the pin is + * registered with the top transaction's resource owner, so it survives + * statement end). The deterministic way to make a queued page "hot": + * an open cursor does not promise which buffer it pins. Pair with + * test_dwb_unpin_block in the SAME transaction; at transaction end the + * owner releases the pin itself and the callback below drops the stale + * reference, so a commit or rollback with the pin still "held" leaves + * the helpers reusable (the commit prints the owner's leak warning). + */ +static Buffer test_pinned_buf = InvalidBuffer; +static bool test_pin_callback_registered = false; + +static void +test_dwb_pin_xact_callback(XactEvent event, void *arg) +{ + switch (event) + { + case XACT_EVENT_COMMIT: + case XACT_EVENT_PARALLEL_COMMIT: + case XACT_EVENT_ABORT: + case XACT_EVENT_PARALLEL_ABORT: + case XACT_EVENT_PREPARE: + test_pinned_buf = InvalidBuffer; + break; + default: + break; + } +} + +PG_FUNCTION_INFO_V1(test_dwb_pin_block); +Datum +test_dwb_pin_block(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + BlockNumber blkno = (BlockNumber) PG_GETARG_INT32(1); + Relation rel; + ResourceOwner oldowner; + + if (BufferIsValid(test_pinned_buf)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("a block is already pinned"))); + + if (!test_pin_callback_registered) + { + RegisterXactCallback(test_dwb_pin_xact_callback, NULL); + test_pin_callback_registered = true; + } + + rel = relation_open(relid, AccessShareLock); + oldowner = CurrentResourceOwner; + CurrentResourceOwner = TopTransactionResourceOwner; + test_pinned_buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, + RBM_NORMAL, NULL); + CurrentResourceOwner = oldowner; + relation_close(rel, NoLock); + + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(test_dwb_unpin_block); +Datum +test_dwb_unpin_block(PG_FUNCTION_ARGS) +{ + ResourceOwner oldowner; + + if (!BufferIsValid(test_pinned_buf)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("no block is pinned"))); + + oldowner = CurrentResourceOwner; + CurrentResourceOwner = TopTransactionResourceOwner; + ReleaseBuffer(test_pinned_buf); + CurrentResourceOwner = oldowner; + test_pinned_buf = InvalidBuffer; + + PG_RETURN_VOID(); +} + +/* + * Hand a one-entry bin naming (relation, block) straight to the cleaner + * queue, bypassing the bgwriter's scan: the deterministic driver for the + * stale-claim scenarios. Returns whether the queue accepted it. + * + * The lookup's own transient pin bumps the usage count, which would make + * every claim read as hot, so the count is zeroed after the pin drops — + * the same cooling the clock hand performs when it sweeps past. A page + * some other session holds pinned stays hot through its refcount. + */ +PG_FUNCTION_INFO_V1(test_dwb_enqueue_block); +Datum +test_dwb_enqueue_block(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + BlockNumber blkno = (BlockNumber) PG_GETARG_INT32(1); + int buf_id; + BufferDesc *bufHdr; + uint32 buf_state; + + check_cleaners_enabled(); + + buf_id = lookup_block_buf_id(relid, blkno); + bufHdr = GetBufferDescriptor(buf_id); + buf_state = LockBufHdr(bufHdr); + buf_state &= ~BUF_USAGECOUNT_MASK; + UnlockBufHdr(bufHdr, buf_state); + + PG_RETURN_BOOL(DWBCleanerEnqueueBin(&buf_id, 1)); +} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index c030870ded6c5..837561d23638e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -603,6 +603,8 @@ DSMRegistryEntry DWBAppliedFork DWBApplyCandidate DWBBatchHeader +DWBCleanerBin +DWBCleanerCtl DWBControlFileData DWBOnStall DWBPendingRef From 418afd025df2ade4c1479c655951cf7b696e236b Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Mon, 3 Aug 2026 14:57:38 +0300 Subject: [PATCH 32/52] Make the bgwriter a pure scanner while the cleaner pool is active (Stage 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-interval diagnostics of the tps saw showed a bistable latch: at the pool's drain ceiling the bin queue runs at capacity, and the bgwriter's fallback of flushing refused bins itself stalled its LRU scan behind serial batch fsyncs. The strategy clock hand then caught the scan point, backends started evicting dirty buffers at full double write buffer latency, and the queued hints went stale en masse — a state that sustained itself until the tps sag let the scan break ahead again. Remove the fallback: with an active cleaner pool the bgwriter writes no data pages at all. A bin refused by a full queue is kept in the (now static) bin storage and the round ends early — scanning further ahead would only produce bins nobody can drain — then the carried bin is re-offered at the top of the next round, so a saturated pool is polled at bgwriter_delay cadence and the scan resumes the instant a worker frees queue space. A disabled scan (bgwriter_lru_maxpages = 0) drops the carried bin, and a budget shrunk below an accepted bin's size ends the round with maxwritten_clean counted. Pool-less mode is unchanged. The enqueue now takes the queue lock unconditionally (the critical section is one bin copy), so a refusal means exactly "queue full" and the renamed deferred_bins counter is a pure saturation gauge. The queue capacity becomes a fixed 64 bins instead of scaling with the worker count: it is a burst absorber, and even a full queue drains in tens of milliseconds at observed pool rates. The backpressure test scenario now pins the deferral contract instead: a full queue grows the deferral counter while the bgwriter's own DWB write count stays flat, a disabled scan stops the deferral stream and feeds the drained queue nothing, and re-enabling the scan resumes the feed. --- doc/src/sgml/config.sgml | 5 +- src/backend/storage/buffer/bufmgr.c | 104 +++++++++++++++---- src/backend/storage/dwb/dwb_cleaner.c | 50 ++++----- src/include/storage/dwb.h | 6 +- src/test/modules/test_dwb/t/018_cleaners.pl | 107 +++++++++++++++----- src/test/modules/test_dwb/test_dwb--1.0.sql | 2 +- src/test/modules/test_dwb/test_dwb.c | 7 +- 7 files changed, 203 insertions(+), 78 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 2212717134d70..870296ea318d9 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3570,7 +3570,10 @@ include_dir 'conf.d' dirty buffers themselves at full double write buffer latency. With a pool the scan keeps running in the background writer while the bins are executed concurrently, and backends find clean - buffers instead. The workers consume + buffers instead. The background writer then writes no data pages + itself: when the pool falls momentarily behind, the scan pauses + instead of writing and resumes as soon as a worker frees queue + space. The workers consume slots. Setting it to 0 (the default) disables the pool and the background writer flushes its bins itself. diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 9cf0e1a15fbe3..d982e71b44e95 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -3720,11 +3720,17 @@ BgBufferSync(WritebackContext *wb_context) int num_issued; int reusable_buffers; - /* Vectored DWB flush bin (bin_size stays 0 without the DWB) */ - int bin[DWB_FLUSH_BIN_MAX]; - int bin_n = 0; + /* + * Vectored DWB flush bin (bin_size stays 0 without the DWB). The bin is + * static so that a bin refused by a full cleaner queue survives the round + * and is re-offered at the top of the next one. + */ + static int bin[DWB_FLUSH_BIN_MAX]; + static int bin_n = 0; int bin_size = 0; bool use_cleaners; + bool bin_deferred = false; + bool skip_scan = false; /* Variables for final smoothed_density update */ long new_strategy_delta; @@ -3757,6 +3763,13 @@ BgBufferSync(WritebackContext *wb_context) if (bgwriter_lru_maxpages <= 0) { saved_info_valid = false; + + /* + * A disabled scan feeds the pool nothing; a bin carried over from + * before the disable was only ever a hint, so drop it — the pages + * stay dirty for later scans, backends or the next checkpoint. + */ + bin_n = 0; return true; } @@ -3922,18 +3935,51 @@ BgBufferSync(WritebackContext *wb_context) * into bins and flushed as one batch each: one batch write and one * fdatasync cover the whole bin instead of one per page (the LRU scan's * scattered singleton writes otherwise degenerate to lone-writer batches; - * see FlushBufferBin). With a cleaner pool the bins are handed to the - * pool's queue instead, and only when that fails (queue full: the pool is - * the bottleneck) flushed here. The bgwriter_lru_maxpages budget caps - * the pages ISSUED per round — queued and self-written together — + * see FlushBufferBin). With a cleaner pool the bgwriter writes nothing + * itself: bins are handed to the pool's queue, and a refused bin (queue + * full: the pool is saturated) is carried over to the next round while + * the scan ends early — scanning further ahead would only produce bins + * nobody can drain, and flushing here would stall the scan behind serial + * batch fsyncs, starving the pool of fresh bins until the strategy clock + * hand catches the scan point and evictions land on the backends. The + * bgwriter_lru_maxpages budget caps the pages ISSUED per round — bins + * accepted by the queue, plus everything written in pool-less mode — * while buf_written_clean counts actual writes only (the pool's * completions are folded in at the top of the next round). */ if (DWBIsEnabled()) bin_size = Min(dwb_batch_pages, DWB_FLUSH_BIN_MAX); + /* + * Offer a bin carried over from a deferred round before scanning anew. + * Refused again: no scan this round, the queue is simply polled once per + * bgwriter_delay while the pool is saturated. Accepted: it spends this + * round's issue budget, and a budget shrunk below the bin size meanwhile + * (SIGHUP) ends the round before any scanning. + */ + if (use_cleaners && bin_n > 0) + { + if (DWBCleanerEnqueueBin(bin, bin_n)) + { + num_issued += bin_n; + bin_n = 0; + if (num_issued >= bgwriter_lru_maxpages) + { + PendingBgWriterStats.maxwritten_clean++; + skip_scan = true; + } + } + else + { + DWBCleanerCountDeferral(); + bin_deferred = true; + skip_scan = true; + } + } + /* Execute the LRU scan */ - while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est) + while (!skip_scan && num_to_scan > 0 && + reusable_buffers < upcoming_alloc_est) { int sync_state; @@ -3972,18 +4018,29 @@ BgBufferSync(WritebackContext *wb_context) (bin_n == bin_size || num_issued + bin_n >= bgwriter_lru_maxpages)) { - if (use_cleaners && DWBCleanerEnqueueBin(bin, bin_n)) - num_issued += bin_n; + if (use_cleaners) + { + if (DWBCleanerEnqueueBin(bin, bin_n)) + { + num_issued += bin_n; + bin_n = 0; + } + else + { + /* queue full: carry the bin over, end the round */ + DWBCleanerCountDeferral(); + bin_deferred = true; + break; + } + } else { int nw = FlushBufferBin(bin, bin_n, false, wb_context); num_written += nw; num_issued += nw; - if (use_cleaners) - DWBCleanerCountSelfFlush(); + bin_n = 0; } - bin_n = 0; } if (num_issued >= bgwriter_lru_maxpages) @@ -3993,18 +4050,25 @@ BgBufferSync(WritebackContext *wb_context) } } - if (bin_n > 0) + if (bin_n > 0 && !bin_deferred) { - if (use_cleaners && DWBCleanerEnqueueBin(bin, bin_n)) - num_issued += bin_n; + if (use_cleaners) + { + if (DWBCleanerEnqueueBin(bin, bin_n)) + { + num_issued += bin_n; + bin_n = 0; + } + else + DWBCleanerCountDeferral(); /* carry the bin over */ + } else { int nw = FlushBufferBin(bin, bin_n, false, wb_context); num_written += nw; num_issued += nw; - if (use_cleaners) - DWBCleanerCountSelfFlush(); + bin_n = 0; } } @@ -4042,8 +4106,8 @@ BgBufferSync(WritebackContext *wb_context) #endif } - /* Return true if OK to hibernate */ - return (bufs_to_lap == 0 && recent_alloc == 0); + /* Return true if OK to hibernate; a carried bin is pending work */ + return (bufs_to_lap == 0 && recent_alloc == 0 && bin_n == 0); } /* diff --git a/src/backend/storage/dwb/dwb_cleaner.c b/src/backend/storage/dwb/dwb_cleaner.c index 0c592e2631a36..66384d506871b 100644 --- a/src/backend/storage/dwb/dwb_cleaner.c +++ b/src/backend/storage/dwb/dwb_cleaner.c @@ -20,9 +20,12 @@ * shutdown and no meaning to queue contents after a crash: whatever was * queued is still dirty and the next checkpoint covers it. * - * Backpressure is the enqueue failing (queue full, or its lock busy): - * the bgwriter then flushes the bin itself, which is exactly the - * pool-less behavior. + * Backpressure is the enqueue refusing a full queue: the bgwriter keeps + * the bin, stops scanning and re-offers it next round. With an active + * pool the bgwriter never writes data pages itself — a scan stalled + * behind serial batch fsyncs starves the pool of fresh bins and lets + * the strategy clock hand catch the scan point, pushing evictions onto + * the backends. * * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California @@ -57,15 +60,13 @@ DWBCleanerCtl *DWBCleanerQueue = NULL; bool DWBAmCleanerWorker = false; /* - * Enough slack that a full pool finding the queue drained refills before - * the bgwriter's next round, small enough that entries stay fresh: stale - * claims are safe but wasted scan work. + * Queue capacity in bins. A fixed burst absorber, deliberately not + * scaled by the worker count: at observed pool drain rates even a full + * queue empties in tens of milliseconds, so entries stay fresh, while a + * queue sized to the pool would overflow on demand bursts exactly when + * the scan must not stall. 64 bins is ~17 kB of shared memory. */ -static int -DWBCleanerQueueCapacity(void) -{ - return Max(8, 2 * dwb_cleaner_workers); -} +#define DWB_CLEANER_QUEUE_CAPACITY 64 Size DWBCleanerShmemSize(void) @@ -74,7 +75,7 @@ DWBCleanerShmemSize(void) return 0; return add_size(offsetof(DWBCleanerCtl, bins), - mul_size(DWBCleanerQueueCapacity(), + mul_size(DWB_CLEANER_QUEUE_CAPACITY, sizeof(DWBCleanerBin))); } @@ -96,9 +97,9 @@ DWBCleanerShmemInit(void) pg_atomic_init_u64(&DWBCleanerQueue->pool_written, 0); pg_atomic_init_u64(&DWBCleanerQueue->pool_written_total, 0); pg_atomic_init_u64(&DWBCleanerQueue->skipped_pages, 0); - pg_atomic_init_u64(&DWBCleanerQueue->self_flushes, 0); + pg_atomic_init_u64(&DWBCleanerQueue->deferred_bins, 0); ConditionVariableInit(&DWBCleanerQueue->cv_work); - DWBCleanerQueue->capacity = DWBCleanerQueueCapacity(); + DWBCleanerQueue->capacity = DWB_CLEANER_QUEUE_CAPACITY; } } @@ -112,9 +113,10 @@ DWBCleanersActive(void) } /* - * Hand one bin to the pool. Never waits: a busy queue lock or a full - * queue returns false and the caller decides what to do with the bin - * (the bgwriter flushes it itself and counts that as a self-flush). + * Hand one bin to the pool. The queue lock is taken unconditionally — + * the critical section is one bin copy — so a false return means + * exactly one thing: the queue is full. The caller keeps the bin and + * re-offers it later (the bgwriter counts the refusal as a deferral). */ bool DWBCleanerEnqueueBin(const int *buf_ids, int nbuf) @@ -125,8 +127,7 @@ DWBCleanerEnqueueBin(const int *buf_ids, int nbuf) Assert(ctl != NULL); Assert(nbuf > 0 && nbuf <= DWB_FLUSH_BIN_MAX); - if (!LWLockConditionalAcquire(DWBCleanerQueueLock, LW_EXCLUSIVE)) - return false; + LWLockAcquire(DWBCleanerQueueLock, LW_EXCLUSIVE); if (ctl->nqueued == ctl->capacity) { LWLockRelease(DWBCleanerQueueLock); @@ -181,14 +182,15 @@ DWBCleanerFetchPoolWritten(void) } /* - * The bgwriter reports a bin it had to flush itself after a failed - * enqueue. Counted at the flush, not inside the failed enqueue: a - * refused test claim flushes nothing. + * The bgwriter counts a bin the pool's queue refused; the bin itself is + * carried over to the next round, so this is a pure saturation gauge — + * nothing gets written on this path. Counted by the bgwriter, not + * inside the failed enqueue: a refused test claim defers nothing. */ void -DWBCleanerCountSelfFlush(void) +DWBCleanerCountDeferral(void) { - pg_atomic_fetch_add_u64(&DWBCleanerQueue->self_flushes, 1); + pg_atomic_fetch_add_u64(&DWBCleanerQueue->deferred_bins, 1); } /* diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 9c61af510b7ef..c42792f10ba10 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -469,8 +469,8 @@ typedef struct DWBCleanerCtl * diagnostics) */ pg_atomic_uint64 skipped_pages; /* stale claims dropped by * reclassification */ - pg_atomic_uint64 self_flushes; /* bins the bgwriter flushed itself - * because the queue was full or busy */ + pg_atomic_uint64 deferred_bins; /* bins refused by a full queue and + * carried over by the bgwriter */ ConditionVariable cv_work; /* one targeted signal per enqueued bin */ int capacity; /* head/nqueued and the bins are protected by DWBCleanerQueueLock */ @@ -527,7 +527,7 @@ extern void DWBCleanerShmemInit(void); extern bool DWBCleanersActive(void); extern bool DWBCleanerEnqueueBin(const int *buf_ids, int nbuf); extern uint64 DWBCleanerFetchPoolWritten(void); -extern void DWBCleanerCountSelfFlush(void); +extern void DWBCleanerCountDeferral(void); extern void DWBCleanerWorkersRegister(void); pg_noreturn extern void DWBCleanerWorkerMain(Datum main_arg); diff --git a/src/test/modules/test_dwb/t/018_cleaners.pl b/src/test/modules/test_dwb/t/018_cleaners.pl index 71d3d3fbce61e..b9442c6d49863 100644 --- a/src/test/modules/test_dwb/t/018_cleaners.pl +++ b/src/test/modules/test_dwb/t/018_cleaners.pl @@ -7,7 +7,7 @@ # right before the write — so the scenarios here drive the queue with # deterministic one-page claims: a cold dirty page is written, a pinned # page and an already-clean page are skipped, a full queue makes the -# bgwriter flush bins itself. +# bgwriter defer its bin and pause the scan instead of writing. use strict; use warnings FATAL => 'all'; @@ -56,10 +56,10 @@ sub counter # holds it, so this claim must be written. The system is quiet after # the checkpoint, so background-class DWB pages can only come from the # cleaner executing this claim — pinned by the class-page growth with -# the self-flush counter standing still (nothing for the bgwriter to -# flush itself). +# the deferral counter standing still (the queue was never full, so the +# bgwriter had nothing to defer and writes nothing itself anyway). my $written_before = counter('written'); -my $self_before = counter('self_flushes'); +my $deferred_before = counter('deferred'); my $bg_pages_before = $node->safe_psql('postgres', "SELECT sum(pages) FROM test_dwb_seal_stats() WHERE wclass = 'background'" ); @@ -81,8 +81,8 @@ sub counter SELECT sum(pages) > $bg_pages_before FROM test_dwb_seal_stats() WHERE wclass = 'background' )) or die 'timed out waiting for the background-class DWB batch'; -is(counter('self_flushes'), $self_before, - 'the background-class write was the cleaner, not a bgwriter self-flush'); +is(counter('deferred'), $deferred_before, + 'the background-class write was the cleaner, with no deferrals'); # --- a claim on a page that became hot is skipped ------------------------ @@ -164,14 +164,18 @@ sub counter )) or die 'timed out waiting for the cleaner pg_stat_io dwb rows'; pass('the cleaners show their DWB writes in pg_stat_io'); -# --- a full queue makes the enqueue fail and the bgwriter clean solo ----- +# --- a full queue makes the bgwriter defer bins, never write them -------- SKIP: { - skip 'injection points not supported by this build', 4 + skip 'injection points not supported by this build', 8 unless defined $ENV{enable_injection_points} && $ENV{enable_injection_points} eq 'yes'; + my $bgw_io_before = $node->safe_psql('postgres', + "SELECT coalesce(sum(writes), 0) FROM pg_stat_io WHERE object = 'dwb' AND backend_type = 'background writer'" + ); + $node->safe_psql('postgres', 'CREATE EXTENSION injection_points'); $node->safe_psql('postgres', "SELECT injection_points_attach('dwb-cleaner-loop', 'wait')"); @@ -191,30 +195,31 @@ sub counter )) or die 'timed out waiting for both cleaners to park at the point'; pass('both cleaners parked at the injection point'); - # Capacity is Max(8, 2 * workers) = 8 bins; nine claims into a parked - # queue must overflow it (the bgwriter may race a bin or two in, so - # the accepted count is bounded, not exact). A refused test claim - # flushes nothing and must NOT move the self-flush counter — that is - # the bgwriter's own bookkeeping, checked right below. + # Capacity is a fixed 64 bins, two of which the wake-up claims hold; + # 67 claims into a parked queue must overflow it (the bgwriter may + # race bins in as well, so the accepted count is bounded, not exact). + # A refused test claim defers nothing — the deferral counter is the + # bgwriter's own bookkeeping, checked right below. $node->safe_psql( 'postgres', q( CREATE TABLE t_fill AS SELECT g AS id, repeat('x', 800) AS filler - FROM generate_series(1, 80) g; + FROM generate_series(1, 800) g; )); my $accepted = 0; - for my $blk (0 .. 8) + for my $blk (0 .. 66) { $accepted++ if $node->safe_psql('postgres', "SELECT test_dwb_enqueue_block('t_fill', $blk)") eq 't'; } - cmp_ok($accepted, '<=', 8, + cmp_ok($accepted, '<=', 62, 'the queue turned the overflow claims away at its capacity'); - # With the pool parked and the queue full, the bgwriter keeps cleaning - # alone: its enqueues fail and it flushes the bins itself. - my $self_before = counter('self_flushes'); + # With the pool parked and the queue full, the bgwriter defers: each + # refused bin bumps the counter, the bin is carried over, nothing is + # written by the bgwriter itself (checked once the dust settles). + my $deferred_solo = counter('deferred'); $node->safe_psql( 'postgres', q( CREATE TABLE t_solo AS @@ -223,9 +228,26 @@ sub counter UPDATE t_solo SET filler = repeat('z', 800) WHERE id % 3 = 0; )); $node->poll_query_until('postgres', - "SELECT self_flushes > $self_before FROM test_dwb_cleaner_counters()") - or die 'timed out waiting for the bgwriter to self-flush bins'; - pass('the bgwriter self-flushed bins while the pool was parked'); + "SELECT deferred > $deferred_solo FROM test_dwb_cleaner_counters()") + or die 'timed out waiting for the bgwriter to defer bins'; + pass('the bgwriter deferred bins while the pool was parked'); + + # Disabling the LRU scan must stop the deferral stream: the carried + # bin is dropped, the queue is no longer polled. The workers are + # still parked, so nothing else can move the counter. + $node->append_conf('postgresql.conf', 'bgwriter_lru_maxpages = 0'); + $node->reload; + my ($def_prev, $def_now) = (-1, -2); + my $deadline = time() + 30; + while (time() < $deadline) + { + $def_now = counter('deferred'); + last if $def_now == $def_prev; + $def_prev = $def_now; + sleep 1; + } + is(counter('deferred'), $def_now, + 'the deferral stream stopped once the scan was disabled'); # Detach BEFORE waking: a woken worker loops back to the point, and # with it still attached it would park again with no wakeup left. @@ -235,7 +257,7 @@ sub counter # and its eventual FATAL exit would touch the detached segment. $node->safe_psql('postgres', "SELECT injection_points_detach('dwb-cleaner-loop')"); - my $deadline = time() + 30; + $deadline = time() + 30; while (time() < $deadline) { last @@ -251,6 +273,39 @@ sub counter ), '0', 'both cleaners left the injection point'); + + # The released pool drains the queue, but the disabled scan feeds it + # nothing: enqueued freezes even under a dirty workload (the dropped + # carry-over never lands either — it would show up right here). + $node->poll_query_until('postgres', + 'SELECT queued = 0 FROM test_dwb_cleaner_counters()') + or die 'timed out waiting for the released pool to drain the queue'; + my $enq_frozen = counter('enqueued'); + $node->safe_psql('postgres', + "UPDATE t_solo SET filler = repeat('w', 800) WHERE id % 4 = 0"); + sleep 2; + is(counter('enqueued'), $enq_frozen, + 'a disabled scan feeds the pool nothing'); + + # By now seconds have passed since the deferral workload, well past + # the statistics flush interval: had the bgwriter written any bin + # itself, its pg_stat_io row would show it. + is( $node->safe_psql( + 'postgres', + "SELECT coalesce(sum(writes), 0) FROM pg_stat_io WHERE object = 'dwb' AND backend_type = 'background writer'" + ), + $bgw_io_before, + 'the bgwriter wrote no bins itself throughout'); + + # Re-enabling the scan resumes the feed. + $node->append_conf('postgresql.conf', 'bgwriter_lru_maxpages = 1000'); + $node->reload; + $node->safe_psql('postgres', + "UPDATE t_solo SET filler = repeat('v', 800) WHERE id % 5 = 0"); + $node->poll_query_until('postgres', + "SELECT enqueued > $enq_frozen FROM test_dwb_cleaner_counters()") + or die 'timed out waiting for the re-enabled scan to feed the pool'; + pass('the re-enabled scan resumed feeding the pool'); } # --- the queue drains and the counters reconcile ------------------------- @@ -269,7 +324,7 @@ sub counter # With the queue drained and the system quiet, let buffers_clean settle # (pending folds of the workload above trickle in with the bgwriter's # reporting), then drive exactly one pool write and pin the attribution: -# buffers_clean grows while the self-flush counter stands still, and the +# buffers_clean grows while the deferral counter stands still, and the # worker's pg_stat_io row reflects the new write too (that the report is # forced, not merely allowed by the stats interval, has its own # injection-point scenario below). @@ -283,7 +338,7 @@ sub counter $bclean_prev = $bclean_base; sleep 1; } -my $self_base = counter('self_flushes'); +my $deferred_base = counter('deferred'); my $io_base = $node->safe_psql('postgres', "SELECT sum(writes) FROM pg_stat_io WHERE object = 'dwb' AND backend_type = 'background worker'" ); @@ -300,7 +355,7 @@ sub counter 'postgres', qq( SELECT buffers_clean > $bclean_base FROM pg_stat_bgwriter )) or die 'timed out waiting for the pool write to reach buffers_clean'; -is(counter('self_flushes'), $self_base, +is(counter('deferred'), $deferred_base, 'the buffers_clean growth came through the pool fold alone'); $node->poll_query_until( 'postgres', qq( diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index b9502e2aa20a1..bd729a8f97fe9 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -107,7 +107,7 @@ CREATE FUNCTION test_dwb_set_control_min_version(min_version int) CREATE FUNCTION test_dwb_cleaner_counters( OUT enqueued bigint, OUT written bigint, OUT skipped bigint, - OUT self_flushes bigint, OUT queued int) + OUT deferred bigint, OUT queued int) RETURNS record STRICT AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index f11114bb7a242..854f8b6a59834 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -1103,8 +1103,9 @@ check_cleaners_enabled(void) /* * Counters of the cleaner work queue: enqueued/written/skipped pages, - * bgwriter self-flushed bins, bins currently queued. The written count - * is the never-reset total (the drainable one feeds pg_stat_bgwriter). + * bins the bgwriter deferred against a full queue, bins currently + * queued. The written count is the never-reset total (the drainable + * one feeds pg_stat_bgwriter). */ PG_FUNCTION_INFO_V1(test_dwb_cleaner_counters); Datum @@ -1131,7 +1132,7 @@ test_dwb_cleaner_counters(PG_FUNCTION_ARGS) values[2] = Int64GetDatum( (int64) pg_atomic_read_u64(&DWBCleanerQueue->skipped_pages)); values[3] = Int64GetDatum( - (int64) pg_atomic_read_u64(&DWBCleanerQueue->self_flushes)); + (int64) pg_atomic_read_u64(&DWBCleanerQueue->deferred_bins)); values[4] = Int32GetDatum(queued); PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); From a2303c22000b09e70b74aef4ff2b04ae03b326c9 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Mon, 3 Aug 2026 16:51:09 +0300 Subject: [PATCH 33/52] Route autovacuum workers through the DWB background class (Stage 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With backend evictions absorbed by the cleaner pool, autovacuum workers were left as the last degenerate ring writers: flushing dirty pages out of their private ring strategy through the per-page path in a cold eviction class, where the lone-writer fast seal fires for every page — one batch and one ring fdatasync per page (142K one-slot batches per 900 s in the r14 verdict run, ~158 wasted syncs/s). Autovacuum is a scheduled sequential writer, not a latency-critical evictor, so classify its workers with the background stream. The class hotness gate then does the right thing in both regimes: under load the background class is hot with the pool's bin batches, the lone seal is suppressed and autovacuum pages ride those batches at their fill; on a quiet system the class is cold and the immediate lone seal keeps today's per-page latency. The launcher and manual VACUUM stay in the eviction class. The new test drives a real autovacuum pass through its ring strategy: the table is deleted from while autovacuum is held off per-table, a clean restart empties shared buffers, and the pass then reads every page through the ring and prunes it dirty, forcing ring-wrap flushes. The walwriter is paced to flush aggressively because the vacuum ring only reuses a dirty buffer whose WAL is already flushed. The background class must grow while the eviction class stays flat, and the autovacuum worker's pg_stat_io ring writes must appear. --- src/backend/storage/dwb/dwb.c | 16 ++-- .../test_dwb/t/019_autovacuum_class.pl | 90 +++++++++++++++++++ 2 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 src/test/modules/test_dwb/t/019_autovacuum_class.pl diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 272a0aa5a3f18..25a7f5ae51c07 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -1082,18 +1082,24 @@ DWBGetBatchState(int batch_idx) */ /* - * The checkpointer's BufferSync, the bgwriter's flush rounds and the - * cleaner worker pool executing the bgwriter's bins form the background + * The checkpointer's BufferSync, the bgwriter's flush rounds, the + * cleaner worker pool executing the bgwriter's bins and autovacuum + * workers flushing their private ring strategy form the background * stream; everything else — ordinary backend evictions above all — is - * the latency-critical class with first claim on FREE batches. Cleaners - * are ordinary background workers, invisible to MyBackendType, hence the + * the latency-critical class with first claim on FREE batches. + * Autovacuum belongs there because it is a scheduled sequential writer: + * in a busy class its pages ride the pool's bin batches instead of + * sealing one-page batches of their own, and in a cold class the + * lone-writer fast seal keeps its per-page latency unchanged. Manual + * VACUUM stays with its client backend's class. Cleaners are ordinary + * background workers, invisible to MyBackendType, hence the * process-local flag. */ static int DWBWriterClass(void) { if (MyBackendType == B_CHECKPOINTER || MyBackendType == B_BG_WRITER || - DWBAmCleanerWorker) + MyBackendType == B_AUTOVAC_WORKER || DWBAmCleanerWorker) return DWB_WCLASS_BACKGROUND; return DWB_WCLASS_EVICTION; } diff --git a/src/test/modules/test_dwb/t/019_autovacuum_class.pl b/src/test/modules/test_dwb/t/019_autovacuum_class.pl new file mode 100644 index 0000000000000..cb3bf9cbb21b4 --- /dev/null +++ b/src/test/modules/test_dwb/t/019_autovacuum_class.pl @@ -0,0 +1,90 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Autovacuum workers write the ring through the BACKGROUND class: their +# private ring strategy makes them scheduled sequential writers, not +# latency-critical evictors. The node is configured so that background +# class traffic can come from nothing else — no cleaner pool, no LRU +# scan, checkpoints an hour away — and the vacuum buffer ring is shrunk +# so autovacuum must flush the pages it dirties. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_av_class'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 1 +dwb_cleaner_workers = 0 +shared_buffers = 64MB +bgwriter_lru_maxpages = 0 +checkpoint_timeout = 1h +autovacuum_naptime = 1s +vacuum_buffer_usage_limit = 128kB +log_autovacuum_min_duration = 0 +# vacuum's ring only reuses a dirty buffer whose WAL is already flushed +# (StrategyRejectBuffer); keep the flushed LSN hard on the ring's heels +# so the reject path stays cold and the flushes really happen +wal_writer_delay = 1ms +wal_writer_flush_after = 0 +)); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); + +# Several hundred pages, well past the 16-buffer vacuum ring. The ring +# strategy only kicks in on reads, and pruning must be what dirties the +# pages, so the workload is staged cold: delete with autovacuum held +# off, then a clean restart empties shared buffers (and flushes the +# delete's dirt through the shutdown checkpoint). The autovacuum pass +# then reads every page through its ring, prunes it dirty, and the ring +# wrap forces the per-page DWB flushes under test. +$node->safe_psql( + 'postgres', q( + CREATE TABLE t_av (id int, filler text) + WITH (autovacuum_enabled = off, + autovacuum_vacuum_threshold = 1, + autovacuum_vacuum_scale_factor = 0); + INSERT INTO t_av SELECT g, repeat('a', 100) FROM generate_series(1, 20000) g; + DELETE FROM t_av WHERE id % 2 = 0; +)); +$node->restart; + +my $bg_before = $node->safe_psql('postgres', + "SELECT coalesce(sum(pages), 0) FROM test_dwb_seal_stats() WHERE wclass = 'background'" +); +my $ev_before = $node->safe_psql('postgres', + "SELECT coalesce(sum(pages), 0) FROM test_dwb_seal_stats() WHERE wclass = 'eviction'" +); + +$node->safe_psql('postgres', + 'ALTER TABLE t_av SET (autovacuum_enabled = on)'); + +$node->poll_query_until( + 'postgres', qq( + SELECT coalesce(sum(pages), 0) > $bg_before FROM test_dwb_seal_stats() + WHERE wclass = 'background' +)) or die 'timed out waiting for autovacuum to write the background class'; +pass('autovacuum flushed its ring through the background class'); + +is( $node->safe_psql( + 'postgres', + "SELECT coalesce(sum(pages), 0) FROM test_dwb_seal_stats() WHERE wclass = 'eviction'" + ), + $ev_before, + 'the eviction class saw none of the autovacuum writes'); + +$node->poll_query_until( + 'postgres', q( + SELECT coalesce(sum(writes), 0) > 0 FROM pg_stat_io + WHERE object = 'dwb' AND backend_type = 'autovacuum worker' +)) or die 'timed out waiting for the autovacuum pg_stat_io dwb row'; +pass('the autovacuum worker reports its DWB batches in pg_stat_io'); + +done_testing(); From 395717d9774a4df665342f61230a1f2d3f60072b Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Mon, 3 Aug 2026 22:12:21 +0300 Subject: [PATCH 34/52] Tolerate pg_waldump's start-LSN skip notice in the promotion test The post-promotion window check captures its start LSN with pg_current_wal_lsn() while concurrent imageless FPI_FOR_HINT records are still being inserted; the write position advances in whole pages, so the captured LSN occasionally lands exactly on a WAL page boundary. pg_waldump then skips the page header to the first whole record and reports that with a benign informational line on stderr, which the test treated as a failure. Strip exactly that line before asserting that stderr is empty; anything else on stderr still fails the test. Caught by an instrumented 30-run loop of the full suite (2 hits, both with the start LSN page-aligned); 30/30 clean with the fix. --- src/test/modules/test_dwb/t/005_standby.pl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/modules/test_dwb/t/005_standby.pl b/src/test/modules/test_dwb/t/005_standby.pl index 6391a4d5071fb..ad2fa036b542d 100644 --- a/src/test/modules/test_dwb/t/005_standby.pl +++ b/src/test/modules/test_dwb/t/005_standby.pl @@ -256,6 +256,15 @@ '--start' => $tl2_start, '--end' => $tl2_end ]); +# pg_current_wal_lsn() above may land exactly on a WAL page boundary +# while concurrent records (imageless FPI_FOR_HINT from the count(*) +# checks) are still being inserted: the write position advances in +# whole pages. +# pg_waldump then skips the page header to the first whole record and +# reports that with a benign informational line on stderr. Tolerate +# exactly that line; anything else on stderr is still a real failure. +$walerr =~ + s/^pg_waldump: first record is after \S+, at \S+, skipping over \d+ bytes?\n?//; is($walerr, '', 'pg_waldump read the post-promotion window cleanly'); like($waldump, qr/Heap/, 'the window covers the post-promotion update'); unlike($waldump, qr/\bFPW\b/, 'no full-page images after promotion'); From eae0a59b78fe1e1f65182e8164a3247b951a75e3 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Mon, 3 Aug 2026 23:18:45 +0300 Subject: [PATCH 35/52] Raise NUM_XLOGINSERT_LOCKS to 32 With io_torn_pages_protection = double_writes the WAL stream carries no full-page images and degenerates into a dense flow of small records, so high-connection workloads hit contention on the eight insertion slots long before the WAL device saturates. Benchmarks of the double-write path on a 104-thread machine at 2700 connections were measured with 32 insertion locks on both the patched and the reference builds; codify that value. --- src/backend/access/transam/xlog.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index c1f467c4df845..afb355b18617f 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -147,9 +147,12 @@ int wal_segment_size = DEFAULT_XLOG_SEG_SIZE; /* * Number of WAL insertion locks to use. A higher value allows more insertions * to happen concurrently, but adds some CPU overhead to flushing the WAL, - * which needs to iterate all the locks. + * which needs to iterate all the locks. Raised from 8 for double_writes + * workloads: with full-page images gone the record stream is made of many + * small records, so high-connection benchmarks bottleneck on insertion-slot + * contention well before the WAL device saturates. */ -#define NUM_XLOGINSERT_LOCKS 8 +#define NUM_XLOGINSERT_LOCKS 32 /* * Max distance from last checkpoint, before triggering a new xlog-based From eacd0412464c4f1a8bf6c789df8a929f10a5cba8 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Tue, 4 Aug 2026 14:04:50 +0300 Subject: [PATCH 36/52] Add benchmark charts for the PR discussion Out-of-tree assets: TPS and latency charts of the vanilla vs double-write pgbench series (connection-count axis and the 2700-conn time series), referenced by immutable raw URLs from the pull request comments. --- bench/README.md | 12 ++++++++++++ bench/time-lat.png | Bin 0 -> 58088 bytes bench/time-tps.png | Bin 0 -> 56726 bytes bench/users-lat.png | Bin 0 -> 29934 bytes bench/users-tps.png | Bin 0 -> 30700 bytes 5 files changed, 12 insertions(+) create mode 100644 bench/README.md create mode 100644 bench/time-lat.png create mode 100644 bench/time-tps.png create mode 100644 bench/users-lat.png create mode 100644 bench/users-tps.png diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 0000000000000..848f219c00b69 --- /dev/null +++ b/bench/README.md @@ -0,0 +1,12 @@ +# Benchmark charts + +Assets referenced by the benchmark comments of the short-lived double +write buffer pull request. Generated from the pgbench series described +there (104-thread NVMe stand, update-heavy pgbench, 1.5 TB cluster); +`vanilla` is the same tree with `io_torn_pages_protection = full_pages` +and data checksums enabled, converted from the same reference cluster. + +* `users-tps.png`, `users-lat.png` — TPS and average latency vs + connection count at `checkpoint_timeout = 300s`. +* `time-tps.png`, `time-lat.png` — TPS and average latency over the + 900 s run at 2700 connections (10 s pgbench samples, 30 s step). diff --git a/bench/time-lat.png b/bench/time-lat.png new file mode 100644 index 0000000000000000000000000000000000000000..c199714017843c0b429a8c94ced9f152cf29637a GIT binary patch literal 58088 zcmcG$cQ}^u|2BS`AtWNAP)YX83|S>RWJLCk?7cT-CL$z4X7$>juIA7=MJkR$XqA34>0PhMO3WXwgC?%nULSg8kP-s~= z*zg<7+rnM&mw>&by1lZMvAvU?tr1F2&)(X?%HG0EpT^P1*3Qi8ITtGj2P+3Njj6r8 zwVePPo8^CB!D?k|!e%>yu@6_lwU*MbL!pS4kw0jPuNT}=s9!1%B}7%66IaHZoRtSC z&uy%KUy|QrWpR5`Q~ut9h$xbv9h<`Uwl58(L@XX-5ih6VnTp{H{9#2$4VJ6jVU54;erxvQ9O)7Qldg9y z@QRJC*>A*_L3DC)>}V5SXC4oE{VqjKcpecXdmb{78_p z*!Vm9^Pd&3o`(-i^AX;75Z>GxMkzPW!^Cv{!-I%c!S!mtsivUIqRUcxmws2qZO6aC zl1?Y~I@!7WNvAG+VZn6&=;(vP@}Oy720{LKu}N2f^^}<3(OWJPLdM70&7*s@9!J)* z?WF9j-DwX=@X6Ux`i1INf}wdb`|Z6Po_lLg`}1|EAB0nh?yZf< zslP{4%)MI3FDPJF$$LUd$@{#yJ4O8E?457; z_k#l%Vx;HR*Sk>cYHDh+@7`(m$`!eyhQ37IIA3b%>MDRy-x_}P*_7%Me>pB8Az@sY zLfD;8IZLJb5C+F&cXzkim8z}Ks`em5ee|i)$jW6myz8^~ds8{YbM^8^ra74s|AD^D~ycfmT zKRB4$oNgN`F(Z+GcQ>TcZeIWU+dC+4=@`bvtGzIW;UOgSsShHqZES7z#wsKWUscQ3 zmK?}_%wgO?j)RGA@w4J0@AIGa@bO)aIMZ1YE?z`SR|Mlt(s!C(P-a2n!3r(e7$fo@V)b6~>EHRH3xuf%RX%KGb}` z`o^HqA9rD>@W$cp>dP9pJ#&SETMF;+Q5DXc*E^F%A~rUxon2g}PWC1qGzAju)JB}U za7`CB6EgZ^pB*?XhS(GNY|!cD6YHHOJkFVzm_(1Lkzb<=cX5umzUq4 z3Zljv^Ex@)X?iE*wy|D!*7)@+F16=A?b46@{667It#Fv+$+_b05Eg}-bLcfmserMHIKzsGeMH3wfb9W7%KK3uqw^kbp#;|0f+As@~1=M7z57YtfMd$6y( zVSU;NNJRVQThE6F7wnh%A6j{l4se*^hF!TMCLw_d`^shZ=RV9eho%s@#`8bEnaqVXaDU)jb*w|Rw zXDp82LSORU8>knv8Po*@9CW_5)gpJ*3E*#inN97z^4_59oe;8cR2WGCd;>3t91}f% z{v6BGQ?twh%>W-pu+QZT%VV7yAWJ-mkQVTzv$J#IXC>E2xmAAogxAq3ufM-P&7C`> zrRD>TlMQ}TzkVs{9&OqEYQ|lesH2v7!zyWPdIuJN zZu9;xx%C=(s?)GU`Mgf=uB@zdS28|~(9O){8@-RYj zlDeRVLVz0Jb82DX3zOAEt&r=^QUDCJnO^}64J|)E1<5Vh`p*u_B@UF_rrr&H=ccRH zYp1dF_SPrbQpAIZNJ+626co5EM=m|Ba=erMG_dSWMjdK8eE4UZsiwUd zQc`>TzPPfw>aqiuC(zN+u^6w8_hnap{X36OsjRuVnY~cGqcxPGiQK>f{tBJu+x^vY z-ZF{Da^w%L#>ZzBd%!%!efV%gMnWwY$)B!?LEiSpwTTj9TO! z45PGWKEOvPN9(xD{M8!q^RcW?v-`q@+;-Wcq|&&U4Yp>!;-g-Mhx1f=1m5eimM|De zm{4SKa@rHyoAAda8G=ul!!@()b#_FZ@9`sF=SP)}h=@q+t5>Nfchi*^WaQ)qT)Loo zn7n)Uj`_xo5A~<~uD>%o6)&K2p-wTgvdU}br-kQLhaPBcdCpbpe2{!eR=s+Ba-v~x z&wVtPU}e8ssn>*^^rO6)Lqmz`>ecP5JTaOuP4RVgBFc<0BSi*n;T$MH3p0Toz^rem z0sx`6T0e*kYR`pIc;P2(332ZTK5Msm)P2O*!J(~kvavM4G&ev0u~;EXrTzIt?bAt< zp|AF2{R$Z=>0@0= z?n`)B%f0C@JKc^?kJcRmZ(JFrhO7N>3;q61Xf*9az4z^%XC)q=dh#@RvAv-f`>Uzt zsd7B zwNQ=OA3S()<&`NqUSqIio>t{6G+n5f#-SX$lMNWy-rti1)ih0D9Pv?t){`i!QJ6cf z6vZ!4hf-K%9zN7NJw1iBHJGEuR%SaZy$dCw;Pm8(D}35{df<*o9DL-Cfq|IsiTu%) zKP#e=1RP#)w8rJEpCIBTe6;v68R}MF9N+~B4(1BHdE`VwQqs(;TljcGd>4SX z7@(Bv!i5W#mA7!X(metf1T{D^YB)GJ{tPNg9eBx5EQ{u!KY*=(vf<(39esV#Py=5A z=7$rIvwNfI-g!3IKPV?JZ!9bA9?vv-Z;Ws3zMu?3*E1?(5uo5UQj2Qn0Ijw*9ped4zRj4{Q|wt?IrM>S6$H3US&zKq{}pBV^9JK;df}U?OF?XP$4aB&k8@NDfio69jK0MI!?p`>O>vALBTWYnkn6U$CeTH3 zIKDny9E7F|yUpV8_u|UOWKaE)UqqtuptuOz*N zV`puxoA;zt9|)ZZK(b!Gc1>^AGg+e8z2u~%tD}{6CO)gWp=NAEl~q;68pXzi$SCYA z|A-hK8bUznd}HWp#QDpt1wI((I8hd-#|I+@D^T}tt+r7iGzpfv2?jR4F2NOUU6eXp z-Py|yo}fK3G4c2F3AjA=J;ApA@884VdIevj>5GAUhp=cA`R}X@M?cO|O0O@wRAMQ5 zV${x&a5Q8-_W6!xr7fLiiD^{XbD&iABjvgc=-8Le+)H{xfloXFbko0M-ai>*_KDN{ zR?#hbdM>EDgT*Ea+Cp+Dr`;6|%i+VFWeU{pnA5~!^o1-d?E7A-GGM?~~1Yn(U5+_2P0{NsCt#eZYzwU+(pp4o5wo&56U`Io`L zlLM>cHG&xLYm{o9j|pIV6I{C+=d=U(3dN(;sT^wGd3=1s?+Lu~1M&QJx2E|JtYxla z?dheuOhPX|6Vukx!eM1)b&Ck6WR)??KqlGu71_*X|H{hB&9xF^LqpSO^_kNs!_Yzo zWtR4{3GARN%GDu*|>UCL0lZUrG}nw!8IY*1$TKD z(o6oxIw>@0D*`g)!=hasS8CQ5h&JqWb^T|hvaTkh+NTZ+OUvQDj|wicZ>!oX{n(Wc zn-o)Vm3NiZ-wF>NKZ6;D2IE?Octpg7^LTi@RVPsMDNtl&WUWO-MRDrP#?iOrdb8BJ ztx^g{uPm`ChiiO4S{uxxTpyB_kuiv2e5{Y)cvU*Td+P=V zTeH<$5yFQ!P(NCr7YYDoE6Ny?>a;#SROcn~>({Rm60dTriSYODX-X_dRjXD3?@@+b zVV_;LZ&}p=KSD$Ota7>l{Xt)>)5_4r7$&trGaVQh>)#8Hv?^_}3JVLrw6~+9f?M?O zx@<{kt`FvEqTX{wWVs(~Ex;n#T^%)1Jqk6la3f%lJ^F0Fi0)k7d0$$U!|<(GRx8KEM+5U1&`mr3^L@1; zC&li{upfMqWhxPL(cRuR=A}M7Y&OFrHm~0rTu(<*4nm<5A zs^`g}wY|N6bhNUM9gVWGG8zhs%faB+qZ(jLNauMG6bLKxJwQ0Kvx(7BO6ZY>YCQyD zq51dst7HzCS37T_qc&!~k~q7%Ua|N{JKYxX;~-J?A~|_5Fu2V*{;s!o&g*#Wkdm=! zhZ0cokS8j~zJ2=w$^g=URHt7@>9)USP%qF$%kNM+dDMS`hk_Qs50+hTZ|}y&l=uqp z@i0o>RzRhm?8i)~VK15ACs)z-y<{Y=U&Ov}EfgsJd*knKVTgrbUE}-QQ=GI);U7iM zpf0|GZ0oC<&?N5fPe&mA_s>eZpgkw;?W@ucA2JjO-K4~VbqV|p6IEe7g}ydXm(TL! z_=_bpKnW7M`x{dTBSnT8aG&<(O-!e?pABj{9xX?W(DY)U*fzd0v9N^g@!h^H^-&(U zB(UWO*qO^?K^b9h&o1KyxYEze%iUCr+ofoFy?aF|Zg;km`=j2oXXk0$%hX8L}vcFh$)><2)>JP#M?KaBg$)yxsfa=UwYxHx$K5-DSrn|SCK z7DfQjU6L%mW{djL(a|5P@hk2wRI_zKi|qShS17LfKZRW&^%MpNIeu2z1J8ui%)=BYdle4LWS*x7 zCIC=`Fk_N%L8QZV|K0Nd8{?APmTy{Gn)~w1$@n)zPWD!W;8{RhG_lGppUCF}>x))M z=;``)>!}cLnMcMe>2%j`A7$fxTV7tChOP*^ocDL-=gLaa>ror+%mdVtWBDi|Dym$$ z=jetyiFr?}c;>p2^W|t}*4D!572e&;Q+>v^W(risXPU3#^Jnxg!WTv=?dS=q_`F@V z=V-aONRkAdTA`XK78}d?S#p?YDo1D#cqC_KFk79O*NV5^@+cJP3m~^wV+onYXW~QFrKXNsis-)j&ZqI#piP%29 zi0KVA5*Mi311O~3rO(vh&Ck5ZK}Y$fxj5F;guxht*Oe^tqcVGMf776N)C6C79yyeBxgxD5d5-Kmo2Eh|CO0HX6KMGR}Th0f=mHw=@R zS43lD;|54NA7IvyLOo0b^R?xPm(of*Xh=vXeXM($Eq4jLC1IL z)oWUWTJ5t%1sHRhu6coSXowm@X#c4D>A^fc>O3y4UTtk{F7PXXBO&Jv8jPZ1<=J;Q z-^dMYD-$h;3Pck^9&mYj0bU6lQSd&e!l-X-YI2z!Ewk*S$p7TR1~V{gG%RTI3lq2n zFMY@BT?*g3cJ#7l`A_nUp&YL3$Hgv*eCOMaj3jC$H-J)Tf>H-U=(7EW{F}Uaq*LN4 zNQV@tGWP@9TbJ-$>_+fXUAnFc*iWwRwZ|}Zw6+HF3kYylYITlg@I~YulOJl4fAf-$ ztNpP6mDkSEaggDMJ7HV+Ri^$U#)m+up_lR|pyKOZ*fpIqUre~%pzavR1B=7hz@RBX z*$$-4xyi};G^H%ZO=sn$xwfHoABMI9h6!=yB}70ts+AQ&1Ox!U58LIO3=AO;)0GB6 zU8=GCmIs&bXlao&(T+*ssoooR>u6~3NfPnWp09+BS^$H~-dZ%@f`^Gd2zx_2sicH^ zFh%@al1oTHJc+UFt$X5jjl1g;`P=;TbaZCLS3OU5eqgQD!PrArWWNwu#tgWTa9i9i zG;V|%B-Y~b%7l!yHWkpE`UF49a+Md!CGd3IoRKy zIl30iiu7$I<^wNms6KKrsNLY0wm$HxcO z9Uh)OwfhDdLb_n##IWhSL+d_$#0hQLjF1LLdQB*YYK^M{&{B2ivWGXOnuh6<-SvA; zzq$2(P+`=76+{u4B$Nfh&5GXzVco~sDtf5j@`3rPc~?k6B%mjXe|^X&B$V{{lU6*M zF0#adX{{tVjrXw9zF)^o;fr7q-#XGFXZlRjq10yhsMnCXq_9vAu4e>|!~DrNww@*s zn8{IoA?QGnGRB-Dt_i7wq*Tffdzs~l%EITvzugmSHciLt>BiUVRLxaS6CL`!*jGBi;B&A3?%lg35q9*Zi@=bI;lY7N z=|h<^Koo13-XqF1wyQ4MZ)uXDsV*{-*=;yST>ug>G8M)qCT&OGInzbBDU>}tJchl| zFos}&OJyF!6;ZbqDOXTNN4+UrF=!tgj0Lpw-&vnX{?ij>D*B)e;|kca=|siHf6azo z3RM73Mll)&c0nKkrFor*o}ON4;fkT=+jSkJn+DZO1CSzK@iy}Y7q6qHIMPhz@Rzl} zVe8U*`cwmkbO>O)4R|@l`-GC)H*WYrYrH6DExz?Yl$>J}h*hZ?FTvtOoe1dY+BMlM z&-yc?c5mLMr4?OweE9I81#mloXPQH!qs35JpK<9^95!^cx9bB7n6zA9T>Q$aRZ;(Q zY|O#;?P$4GF>q_`6A-wO7jvPi9r8mhlWAQP$Yd?o!>18E}D>1%I>gju(DS_ zWKfam%ibz0$QtUQlR-5tIdB0Oa{qxFfw#kAZ+iVo@LSKq@^TX$_f4$)>i6p>(B_xW zPgI@(TM0Ya9km*91=Xl0zu{U9IE?SCyeuTLAP3>?GfHX*B=w z>3h!d>}-qXb&wz9q0dyO#F<_DIFO@8X%X?KANnT`QF|Sy)a<7o@EU$0N)oh%D;V7C z+>&VKqcV_WH;t`+d`-kN6q^1IAjoBcI$66TCN6HX`Ad@9Vn|W2X9tD~ilNWPkMy8# z8gDqHe)wQ>a_Eqjo=(foP6SoBx9UaYJvW5BkW5Rr$gQ9(}FGFr~WtgkEr6vBSusGr@fI__08l5mQ z9vqCLRpUy^z`#&YSl9qtgL%;F!nM2g@Hu-xoT{j(*m&o5dd&ZP>@!x1Vz7Y2(o90u zB1j`G@LKDoeuikyc{BD_>*-bk@FN-r2g%?e`{d_-e5`=#HM3VabJ9Uj(+V|jbM8AO z{uSwHz0&1dc@?8GOlPjz`wV^-}Rr0m3 zf)F%ttq4%Y##$9jkL0T0j}M+Rj_n=?{4xvntLGe zC<2oE<>xCH8r}q=gsG{i2^}t(Qr0Dy!$9bI)^|czFVGx09{^)R=Ecy+C@^MOI5+|lkgzXSde(UmKBt>la(`gGYTbu3_e6`p=?%VicD#MTyg#$y=%QAgrx2{vASiQ_AQR9H zeeR_PH&*T~x2U`OU7%{x%w|}${tG0OlrOddM53KJ=^QWueQqG2FGF zUgCy7wLj^lP@T{AxGb7w&Cnz`uvQQ?-4?t~>v{~v9?}9|xnqtAz>Pd6ly8hSoBiK? z3NoKv((g-2;UFUe;5H32jkJ_ly8c8t(~FBCAT;z`d0?ahP79*&;?=+KR=nMuA|7-b z#e|td&_c9kOHn|rW9&)Vb3qh{wI`8g$+;39 z7ZVczrr0k~uN$CSd9QeTa%RRK#!eTE;%fd26ciNTIr$|eC3XIK2erK^Rq`dCyHJ3b z55L`>w=WUlV3Knh;?$n(YDW98~*}iLuMSbuqU9)t*6FH)P4CPbqt#6 z#_!(}P>O$tN6>?XAG~whw3fCIKL@W7ai~k5{rCku=|0HFG-NLk?D@U?;{u=t!mD?0 z%9Hpb3VEitOfsU_ApHRpjUFhY;0L`2z1|c|-|%Zf=3mSI*ycDtxrU1(HFdE)R?H7-%S9w6t`@b0OD5-2+cJmRZ9WCTr^WV4KX1 zTE0Goq~ii+scC4X=}XqlLD>VH%K zzeBwTkGI;EmzUSs*?Drj#*NH{?OzPyRl;NA2R}=nU9R2#bsieudntI61FyVt4vs)3r5g8fQQ zSFn@hzXZrjvhVRe?WEr>t9LQ3`n(!w+H{ zSMVEwctys?<5p^YivA_c!Eq6!ZmxX+s3=XDk3N({kPt@~7!D|?bDLmmJ$)gZG#`CA zD)UK0-4~sYxv8gI)H^8$nHRo`pO^?QiElrmej9Yy<=!7OSEzDEcZ+U1CtyNsX~tB= zBZsh&n|r(L`t|F0xjBlrQK;)tQ{}3_3z0+!)T4VKpOnmklp!T8-79#;%nRO zu-j7(waX?IRh48z@^%lWjas$@j7WEqk()m7Fuk@Ug4{IXu={Sm*W3qT?uhiGt|7CB zsxfgl>Y}3F+HV5RvlOi5*PY^pPj^GxqwNB#$%;@7TznehkOHd-VeJ~&$>^xVy>+WY zN}7N97@Z)`;F0$KI}l!_7uP~@fny#W8GK$b*#183_<3iHeHu&%V)N2lW#qOx*@wECj?0t9AbF zxG$q^*}m*h&qA-g8ij)ojFZybFXW+rd$7FcnR_wJd9 z-`-(docou}x+kh0pYp1O#@I$P^H(DpI|`-zCCc&n4bHVB{~s&cmxBMkP9MvZPoIKT zu!uang54CofC@jm!um@%D?^C!zYA@>r^&_FWu$|z!rco=QKTAOl}WO~bv==pn!2XflJG ze=Fq>*+ht0(EVN%2M@3D6`c%Z$;c401f38P(n(RxDRlswBnZlGLu)HGxM~dmgEW`$ zdNLo~$knNAbr_fj}5Qnctq(%21)?0S`+!$qU^HeRdRU-IW3>e$2$~CH?1r z=VM$z)j?G|@5vn?HA!lN5Cj68*8lp7uEKSP5iXC7Dkv{+10#5&Q*hmMndjLcKAU!R zy-Bi1AShD}ot>ZRf7m-XNY1v$upHf&_Wpe`{H4Nat8RwMT`c0Blv^jGkLrJBp(tBW z?|Rf}TsH{osK}U@be04T18g=_ACR+)O3KO?$ucNIUmJ;KU9L7x4wCy$SZ!;gDE zf^csnN1hV4qC?;2YEY#3GFCi7quxN z)%zk0DY5^;g}sF9R4{jNzeeU#Bh7A)&Yq=F=2@`K|7QU^WD}>f6LYR6#vc~H(8n*hLiW2&t0)LkeW(2vCX;&p^|0v6oIZk?$fJ8XAP$;>v(*P=OW-gURQ%o7<4$XIn`HDsgKZ zhe3%E2o3chEYrOChK2&+9faNn=zw|(4IZqHmG|mzZesekFr{!cL`)O0mN&g3!KLeA z{=4C&uBnpx*KBtR@<9Ug`1p8oe*PT9H|*DbQh){9Hv8?()ZAP~UX}BvzJE~A4G3jI zpN?#Amovm5w0v6nBn&<`EhFQLy(h342Ww;f{4fxHh@_x^b~6s{S+b_T(ECxxiE)Q- zrTV&`l92*r}S)n za?-oITak~C&-5D<@?4KR*jzx=R#Usv!D1W2aQ0i<5s@kXFC=|T6ssxv zlEux{wXv_SFVn+@%EY=B0wehkEsJ@SG|^V*5*zMyXeU06_`5fiiWYLK@riKx@t-pW zterHTu?PCxnIXd6&ihx#8E#$jkGWV}RYlwVv@1-mmB?F4Hs^i9c=EsZAK%7dx>21d z!FT4uZ;x9xAETzC?kKnM|X94CN=hNE}|IE*PAw)r_`i{Q&)SPlQb7wE_ObAJ~-U6^kHXXfV!%to!`6$7*}+`nn)<|m3os@TIXurimD?0A`dw;$k9 zEz19kN2_|ndl;PpC1?csgSH%QLz=n&`u}*ebT;wp-(>w5Ig0%MQ)JQL{>8o+TbO$2 za<+tTFaPbSq#mZy#a=e`j_*+1fUcrk$947`?+kH83PPTsh9y^HnGs(ZV zgdlJE#Yfmsxfb}9->q5<_4;+jyFU{YNh}FfK7;G#BgL5Bev&^=q|^u(YcgMcsGKhy z>MJ0W{@+JR0j}^Zfp+tv8UgCPySO+p?w=81V1PBCcgOuEku`q=_4#ZRs>oFP-v_4) zgb&^!fBhaQTjA~|(G;VSa<;;HZPUUMiksI(KCU;O(&R=mi|1zdjMO=qJ^W2xBa{dZu;pCgw){dB!z zXx6BpV{9og#_)kj0~w$|$m3yRW2Yh>kzb2gJ~3v_O+#-oG4Q zpj(pg`dxk*(pGuhAUt`_{})KVJfgT@r~6pt7>E zkSMT#{em|Vo<~7Mo=@r~F_3Y*_3CXxkpH$kSnSH#R&kOVfEmO4QlS?F0CxBU_LNMMHsD)0{&M5m4w0R<{p0oQ`_aGLkW=-fSZ~a;cv$ zQ|XuVr$>JmL3MhrIbSLw2SRd%5H!vykSZlXm#~Haja0glWfCxoV2SHsv~HKp@)3@Y zk2~)y#Weh2cXErD#g3QF@JM{k%%xamI4pyixx#SYRF(`ylgJdrJtTS{a7m1QOfcdf zwxk-wh{l1-J5u^AC@t+KFw;a~k1G&EZiarn)Pa+e6SR<>=WJ0@kx@~48+SU!JfXLZ z6Y;9uEljGxaNVU2r)RrP8^(6%Et{n@?U}jx%ma%*hv-W5$yq1~&FdWewN5(2hWy|s zrem_JBB?JAyOGW|<$rH_)w9nGi!MbkX&MnB7LVoIePEV)n)^?0eEvVu2oZXGR3JVDI*nUg>U}7AK)GGM z3ZOvsm;_G;y4F_>9&f8^R2UJhb~hO+3WHVF(7^8e$1E@AOgy^uEJSd^p-mf#c|pU> zz<`SasVj}eZ9d5fh4MN(;muzIzV}`!3r;GW^E={(<&i;XBz5!8o0=Kb9}_ul%{=rE z*laUwH{|SqBnb}>4>_Op`?6{fi84V6u05EeOv}hX|5E$16pnFVVqv9#9y9V8#3FJI zeN2!llrvYq`Px3eE@f#ayw;{gYS>bX`qs|;pE9zwi?HZC)elg^5p5M(`3RuEH*Vj? z0r$QQHh^NfG^yoNaGqhWiNXsZYXXlz113Ni1lg&CTrZWCm5qGX(9jqvv!sM(5d4$Z0QSI;KVq4*O@Rk2B7h!m= zdWS0NQr;xsa3$mGM8ChqLjS(#kY-v&K7nn-l6&)GakBr^zBKhmM7v}*C8Y0DA9T8x zztN&mN^PnqXZr8ZI;PO9u#iwiaFeA;;Wu1VPc{amfDUHMB6A;zX)e*Ap2WKw;VZ` za0I~ufK*hP$1!v?NHj^k)ST3Hcjci^TnyfB`K*v9O|)22)DyA1zo0j>s;^2^=DK4p zn=JhFXuplxOX8z^Qsk>wkmDiK1$D-lH&R`D96uzYf>E(H#YOKg@?1Js>{|e7DkO7( z^8WqX9uaULrQ{EhapVvYBm-V@zfxB%sC$PW^51%`ucu1XYC{EyC=?2j-2rQ#lv&`S z3Q9{sehxffxk;wRq(HO_8=P5M`EPqDRmctw03l&35b=uuvQ zKxaat_*1m(ee2-6Os^GR{#~{%f^jwKTntG|m@^NR*)jujO{F{k@>HouTu1Wi&dz*D z!sarYHVp&A!}$Zqi^Uxgr{TPIy4<9l>8};zfpd2*n}%tLb7je%bt?8Bxhjh%^l1#Iz zJ0XK(JhA2^47VK6S8Yzh0H|+@iT>yKoMcBXzaAv&)FDfYrJJr|Ey+7ReT2(%(Ek?! z{ncM+h^j-r2THL)R#~JzSt7mouNwi>{aLtl{a60&bV9fza;gp@)!}fCL8dh5(uXIf zE}S=Q2O@X==Z$0u!pZxr`YCo4gI?^P6v zt%4@)zIKq@YzE!We}uZ76;>})q!a*4U<3h5SrX3973m|AFCMZekZPao6=v~Wymt4c zC^=&irc*Uky=0Y;F5>?zR6fr&5=+)@f%dTm!vU8z6s78P&$)b{(|U6t0s#f0Mq z9vvJLy1>g@?#q-g2}fuUpaiB5yXsMuqh*o+WJ6n__1hYI0K}>rv}X5YwJepP0zJR+ zc|_+RV|fxczM+&U|Cnu3zj~MBzj$H_lL`y}f{l$0+4;uP!(}#5LNSnVB`j9H@TO$1 z(-5#~^uZ^(t@bIgYz<-})8ORTD@ zk{nksBcfWsp`pidi(DhfQ1K>txHS>{mu4xM!Yqj#Z(u<62!PtDdDEKT95WP~2)<2t&Mfh@VsZ9Z#cZ4N)ZGgygxPoP4mEZgnlb0S9dQ7EU0+ zj{@pByNc908HWq5&VCt2zNC1+gF9!FGc~XBl-V<&lc@DW469pQjskEz11T zx_Iylmr?QGkx!{8c0s}GC_X{KX^7xBZ_R{ggk`DazZ>5Iw+1yVjzcgE=jc$nzZZJ$ z*UfFucOeJKz`+21xPUL`PT9YuGNmfrIW3rCli8g;Gx8VYr8JCHiQoX7UY6qw6DKDz z$bk>QnuOzW>F&#P^z_qUHsTNx-iH&Rq2pd)|G^pog&wMF5PareU_>M5@d%#NM}MV> z{#p_d{mKa2T(f-g-h)Y(&S|$tc)uA7|IuJ2WV@5z0h0}4v50U#;jx$0@Y!v5MHG%l zLJ2nLua&%h5pXO3a^9xj5Z@kxzp1bpz$Zh4(8l)vfg+m5N$$=r-JB7srDQ4_s?Cd` zJ6gAt^e{!d55=>1up#~zLv&#t@Z!{vZiulh9HEg-SJL|uNsHuDW7%}V!8In&tpm$+ zXm}X;{ry#)o5Y*w>?mkpuFQ4B`zZ(?V8Z$lblqD^THpPz83P|)y+r?Nw(3SuRYJ}q zOZK*tZub6cV#6qpgM$N%Hf>XCkw^RQjc4yA9e5ap{2!if6F216KzTdDoTkA&Y=v}F zp=r-`*iB8kCx9kX05Ifm*bs!m4p+6X}l@lU-Mu4I(!3;D$lf#BhE&F$B|=4vIsQ<16Zvo<-`aLg05d7h87Jq za_PY8<(2SP?Ky_lbwyJfjV ztlte|`ln&4m&aYA%UB8V>H2Mg6Sqkv+?TA^{*Jsk?Y7Kkv$WR**;By?E`yOD0WLZ; zM$-V%b3zkv3S$3YGw91zppF1U`w;iu8ne289~&V6;PsgK<-q(=f~i>#y4_1KBWZ8l z!bW6VI4%mAIjg9ak22Ad_s-F9&u`Y;Zu*Ay2V<(k^p#$lRvnWzPRDfrJbT4N&Z8~W z`t_U{qQW@yYYZ^(6pBeY`cpw>)YH?`v^OoJXB1M~`k;xUyumeJz6&XJx1=Hnb|Z;S z7_Dzkb)f12dqTRf5+eXxF^DNygYk+m2y`D5966DI(N*M zj^be#Z`{`fNfmo}XlQ8R=VJ^Mu!ff%H<-?6KW4c=xr-C8<+!m!lXU9*=X1|wsBo+t zs%4OPo3Wz=^S>0NCeuqa79|+Hjm!Ts=bs0GYIsclCwceblNptG9 zCej|8j6A1%2$4{V{mR)N$%Ico70E)XBi4U<;ZBq_&<$SuQ|F#tFD0#4ZKf5E_o6Xoo1!T#{ruCZxjoLoA7t?lOOSht z+vZG*oB1B;yl3W)Tl#02HXmA3O`1M)j+=@!R=V;+3eQlL2ssPq4rfJh2nbjzI161H zeB`U!_EH(d|EMCtt-Ieio71mcGassq_24KCd~FPc1qwYp1x22bH~p&zdqhS#PNC^sh+86wUl2d^_r6RNoaLV2VNtR*aig?&J5Sq zL+}sL7{CV5|5;&E;kf!XeMIU-h)DqBhF*SYM6z*(xSnnY7gA=vjOuTOU^b`EVX8f@ zX$jVTg!{%g)-PJFbE9bklkV2q3p4|JLdh)?5N+{D8Fhz?jF7elu^j;<3?QfnOA!|- z+v~MQgqxe(<(b?>Lk}N4CI94+h;{MW1C!Uff2p4I_(`VhvAb}HT3K~I{fZ@K=4I2D z4t_O|k*$q+A$RuWMPWc%Nul}@86_np{s4$~ zdLOGyyuO)4-$U2;w?Z@3lQ)H)6e`ZX#T(2E#}F8_?G|1e{enwf`g*JE==dJ{WsSurj!zvxR1@)re;6%09ebrsb)d%^caW*t$B6Ad#xCq$P*CCmV|az_ww_xvYq+ zwou~e)aGB)xI;Vn__kPnHU3SL8A=^%YxnCU)6@f6O!Q{IN@x%F3SYA>;BdcSV93PK zDg0`>J1;TeLh?$uXwz0%<|HlatFl+UZMlMa$k#$Z9q2gSlP znrbE9hF%WeOtFmZM%&FYpX$Rb9yXQm!EBK_)ku{H`o24`G}YJbps~m(ZIq^!r@6=S z(NJ6C%XZo;Gko}#j`DW+csLPi*VoM$F^I0^H~+OOw#WpIq{5pj*9>v_bR|XDtf@?N zbEsH%#b8P?^geN|F0*lQoZY6odt(^(GQtFaIWK!Whf^&3hs*ijhlLnuOREn=yqt8p zBd)=HQq%w6z-bz;&f%&#KWmg#q})5tQxwCs6kE(;CL+f;G_`zft-5I7Bk|@BKknjD zDQOWP_3$MI??EwiO@0R6BP3GIJJ%lK;u6~a5+Y3byD;LI#q+NE#!*-LipGjA*1miK zW{~o3bG^wS7iNbuVe(=ZyOBbNCKr``)5KS2q9iWTrthNOl`g&v26Ap)q^mjG(TQOr zhuA$|tsdySItGdcf$N($7wTkQ0ig{I#kX@*kPdcI#BweqU!A6ZSoX#B+b3UW;25;Q z*0_l!%TfNuI&$QRMTl~0+o&)b8@O8mZ3p7m^SjnTRUd2)E^HCw@L5o;VX%EnTs2UB zOkTQkr{7$l+%+M7=M5aiasAs<@I3z+467u**iA`HLiq8cFrgk{Rd!mj2}N5%K!3^k zp(K^tDuK(0Um|7tGm%P(?_~r%h#&9X@UENQebr~TR)&2}ioGbNSmWro4Vm7w^{IfK z3H8TiL+zVZmtid7L?Hr<52J8y8Gd#k6T#J!eoCtC^&JmTIG1;Dq%G7*4@8; zwa<#K_?)U2)LbO3>3J}FnfXZx4icsU0>}=BxMWHqUPq2VhZ|Vkan_D_4oc)HR+koP zSn;yr`Rm|&6VN(4ZwK+_Fiy4f;$^PH7Y`~~WaG)4sg(saks_?^zLi^;{IlzuGjg$y zMni`;v2o3en5>7~Sg_7&zkD^)Wt%FfC=Zg<5oew$ST&#?uWtQMB{SMyyZ$v<(zqNO z3ri1>D^AFb2T|xC=qa-j`}%$MtQM^?|91WDKlNJXg{)HQ(S@jBneY7Z<)n6e?Bf?4 z&@E_)Z8sdrrbbV-t5-b*-uMUga7&D9nL9WZg&l4;6)aC&iY_}5k_exZX|mao944o` zA)}_Zo5H6(JfAd1G--Y?l|L)gyl;bZ7ANe^M4E6KHCv3ukaN3*sOMW}9Uf!x{775= ztp4}~#IaRU$2DX8eN0Q7Pi)J&fs0kY8zn5zR3|WCd+*H%;YgeGol{Dl`%Z*-%bhv0 z)y3D*qxYI=9(5`lu$9hEuF>2REq<>}cYAZK9mm~Eps?SIY~uJ`FmF+!T_WB!ec821 zCByt5AALxK%^7B6JUnU0_DoMtG+S0^R-i#%7%rwqgi`qagCwssd*UONX(K62@1x&s zJ`hBV@8uj=*fzLy?f!Q0+9}@<<=8Uc(wMP}&IKt6e!Yg_wnaYIAhPN^kEH{zBvS`V zelwb?*i~|(NNJEuvUc)x`58y)-(#7m$M$v2&J8yxDld(EFtIK?Q;DGCE_u-D@&}pK zjb?hQOX_a1$>|#jwbu$rFRyyMusU){4a^7A@WxBMp^P6=-_jg5#>rBBb7s~d71)C4 zD8RW>pi@VfzmcBU;cZG{4vWvBuI$zA16wj}V3Bu|&^XHn7mO<~u(`cRC*eVYO&G|lLu`dpe7=p>pT z6~X}lK~RX)aebocvt?4fwJsZqU0*;9%gDw@ITWoHUVe4gzPQXL*Dk+M*iKs;oZpsz zKI9+?-@wuMD@NUp@uH5S1KX?JSkf*7(zwHC%<{8~PBen&XWu*>W3TjMT9^M2DD|b` zyHA~zO)|E8CZ(<)ZG!f6XHMca`uB}y$r>T{teW$Dorgc{j!v|%tUF)A^(H;J`!a8} z^w?s%vt|_EG)T6`sw|&n)BJN zGF!u`rtUwLUI-KHH(84eU-i3eW3TlF5&OW%yDuT(pFYw^a`kE$a!&5(t^q#8Qu~g& zx*ZR}oVd=-ed+7h*MUO3$tWG?BK6hjAS)FkN1yvh=`cB<4%y$-BF@j%ym~H|a>l6l zY0zm{Vt2Jtr-00&20kzg6Wy2PeZvoIxizT5CEG|Mvu4hopR0JWv8=8`MZO+Hv)5>H zR!n-n;82`xg=!XqUh{M@&Dc5hj)}tYls)mgwmD1zZ6#V`M;oSHM_^c`e$_+^v0Qv| zu_>(^-+QV{w}U-p)BOeRpvgWyEOJgD^@s0wAOvAUQY+Eu6crd4_zS))#|I8XgOVHs zWw4>81q(8V;>NeUZfn7pzJSHh1C1k?rQUm1aKz<)jW#r@aC(Q3jg1ZY&MG)@grvlN zO;59Ic}fdLDcn&pNEvC1XCzcGSZD{zLo{t!9T|GiwqKW%$2h8Y)FwRQa=#VKYo;T& z>rOv+N588idgZ!(oVR?h!f`WgR*NP~URA$&&eZ-$m^j77GhY&2cf{>e#T{N8p`|4S zZzXb%D|}PE9k!X8oq=l4zYmk#ATHkcPJO&HN4B%&-7g>4MJD;JMLT%)h6hcDgR19= z(XBb^upbAZ4|!sku|otiXqHH*CFF4L_0tPW{p7%6)R1kU)Wj{G^Y_^P&MPG^e;;%K zN~*ze7}|0MrJtQm=*X zlnVC)PU!XnAZ0}h;ud^^g*Y4wwSn($P~v!3c{{`-C-v)WdOD$+;-cI;3Y}E59i|Qi%nkJqa5996l zuGvb;i`>c5eJLs@?MSE^jprEmqwlI6;k;IXx(DMCp=}1|5!bb!Dldk|#wKSC2N&J4 zKK`$qqM=u~EbW7l$}kAR5o;HBL8g(jnuJMrFbUC;r{Me%d=pv&NG{f=$1aJ2PT_l9 zm*AjC6LhtuXyhp9a7d6dt`P5Gc5EX+A>ZN#-Khb1dx+$Ps-KvkmWgF<04H$+Y#8;G zQfa0rt2-)P(HX-2f8q@sjj(%wB@x(8U}jCywnl}VTJvm&H~qY}W?kCFA5cl1ULSbg zs_nHeP=gUxuYj=2uS$LVwHmA!H3H>P{|{Sl6;);T#S7Ej-6hi9-Q5Dx-JMd>-AIRo zBHbY%(%sS`Akq@j-SDmb{?B)F#<^iIxMA=0JZr5ve>HcGsd}cQJKfIbLTFHXc$CIZ z2b9(4{5)|5*@TIsB{a^VAInbKzDRoo5jgyC)BH~3?|#%WZc0s8a*5vgm3Ap*umg$( zF`S8wtg%R0>5D!cz_>wN1;gnlMzEXUb#?!sDT0U_vxI&0J`k>O3Sd`};Ab+Wk~74= zfPaL70^rMLhR?rwWU}E6pfjUu?qtF7)0W^pCay~$zyY15W-_S6ZruX$T-D;Uv|}vO z9@n>=p5F#w%2V%LuG3%Y`La;BS1jbC_2GcmTwbq3bN74^~uW*&GFc%8aj#> zQ(e1)xb!IgZw9&}n{3f}%bnAk4#0JgM)~)vRli(V{2vHM^5nu5wp3Fp$e!>WEQd4t z2>US$zUKf-ORF4!O22&h!qP0f8U(O%B%q+zuG9vU3xo&(VFL2ti>z;MhCvWY&`iP~ z-+&HfT>t|}(zY{{OEdu(wF&-$@GWc9s>6(1NNBld=390UbRyHg+h;&XHi59Nm0{g~S1*;3@?o z#J~h?mS7IFtwg*Ico1PDK%auG|?{pn4$6M~b5jjoe_jL_nnVOjMyLZHK6A zZ7P~rYMLBs@RJMt6l`ZveY3X{!Wl9i)B7}?Dy}2X3pFMjSHH*p`b0FpH8+asYk_nL z0v=P3&Sld`x8*1?nkVK(T1JA?81=Omt%a&`rEzg8cea|`c*izMR261l)2@ZU998-(R$jLY4QpvfEkSyTy2f7;;Flv zP*tcA=^LU0<9n04#M?E}?k=v)gNCO83!CRVvcAP4WAz0`UzW#X)}*gc@ds6vqNZjQ z%5-Ogl&$;jrO}y&d%&>R6EDIjvET)Q!x^E+7(*EW0lAS^1!-S>*p+GXaURBq%Df$a2 zK>$MCI{okrkU9>_AvDPO57wDWNUMMAfnDJae)8f6NF2zvik5YkV*)DwDNdRXy%v&+le9PCk4VqkZ9qX=#j5e?LjN(y!NRJQC?+nq zVb1e%voYMeDoYSz&$55in=9@4*Dq@F%QVTyY6tsWK8I)?`^L&a78l2q%LUNXZwuQp zzlz_mHrjyY5at|K2Hh^AnF4pkPdGD@J9PDlL};XVU5o7neqZb?>lr^wYV4fFQ+{#x z@o!HeHEd9@P=F~z{Zg128%fr;xz_bMypiUZ+I-FqNJEiCVW^J;s}`57Fut2Em5J&=1-j}yz@zShSme;QKF&Cl54jAa)qwq8SPTgh#P-M~^=A7GoAsPD?I^xmC z??lY^eg=OOT@=Uu|K^6v_iwzvznPtORSVD&UYR1G1R)m_GR@O8I1jue#y?wYBd$eT3b!-S}}!$T=dV(TvnL^u4M=W z{mg*m0x}U_ES%V9#SWk-s&AN4{niFt2p|X&D4rz^OLeNwK%^VcrUB9v1`054i9o$( z2@dcMDknf~Kx}$Itu*S)9zOtJBi5;WjNooO{1JHMC2&vG&>SU6Z)9FRb=#IyF+wr>m)HEc6+|3%%0KVkp z#sg>LarbZiwhWB7$*4ZPlL9xRjn~&>UZ6yG2#3~lLAG~*e5{IS!Qo6$Peg?-LF3StukuG|01J4Gh!jRu(eEo)66k%JJ! z-(3EI%Qg@AB8h{Os!oPJO(f-Yjd$GOew?5p>rSE{} z5^)5D4C7}_ZloGBRdgwyxO)^I{UU8^L|jcn$%>AuG~ud`h1cRA-mcF}3#y5yZ#{0k z1O2;AXRSAptR1!>(8(F!Usm!YqBbZ>! zKaj_~VlBIhYv-?c{TROVr{)2rEz0HL)7i4?Kdk;0hX=(T_o&@YCheX_%d_mYxj#$< zKCCVuBf~C8>8@N<{UNRQc`~Q+-kV?0adWslw$ov=Q0S30= z((xfB$XR?K)N5nf1@AC72r1P0`ISGQ1vawfPm2E<%JtL^6mahJbnrncBo zE^!M8Sq=y+qBSsS1D0w&-*E1Z@k^Re4O8CV)0^a2mQ^TJ{lWZtJ&?Y<#?>=H1^iAy zzQBtQ!To`53I-y*g=^4D0Kv!zfam734+g4>-I73lfS$ZNStSS2tnBr*AY2ewrT??& zo$=w`>Bu&h6}Hma)v}_-jucZR=%H&Zpl^`Xcy`ISPy4{I>T-Y%Ga(Ig*6sPiL+5EL z!}NjQNMWe!fU$g=ap*`lM-G$o(gWv9pqKx1ep##D0o? zFHOT_NG}pKGW(XO^9a2U4X!2BMk`t{(Iw5^+Do_EOJQ1p0` zWO{^gWm$>KraHIew9cO*wgbNc_ZAk}@1!t+&jfXoNsXU2q@L3NsYEAA8mio)892yLZ#0AY$bN_?W#yp{^oygk)0|z^L`!bKaz4Mpmsa)y6F=rS2{W)FJkPz7=Ko) z81*g+bz|Wzeg?kIK21XH63;JYeb_W(S=RjqOXx=$xBKgSP0s?6fh+kYNoZU_ayjQp zLHH|0DiiDaq6^O<$#q{OArmN@5^s5ep6KwM&ibj?o!XL({_Qy7BUxZQ{^$&Cz|JJzB;~!sxP^=!1RLaSv~D7T+_zP)jTn+^JVs0G^O# z4JeXZKEXlOFc96psoDwAU$K{-?taIDwybLAWS7CnuOJQj^lTg<~xpraZD-b>28IXfdeA_$x{oS*%McliAx+koEM#^$--kPoI%b3kIsNPQx)$`Ap ze{B}Zv_K&p5Pe5{A;L&5)Ads$M>IOdT5{A{-KGSr=SPkVoH70>w1qeHauVK7jCpG> zV`iO=+pN24Y8_Z|%k%S69-K72sFzuJJL{FQrw*|0Hz29?jlnuE;?fJ}%z84EAaFFn!C8 z=8vqq8LOOk6rndgPQK;6o!7(qAWCsGYdu{JHMS(s<9Y2DlNb1|u~P8nBdq;RjzV`J z45-ejE;jU!JW*om^)BJB%O5UGw0byMj*gPlg-&<3kqX05(?Y}X2-=Q~OJgF)@|*>~ zs%0M0!tm7CMVI0qakM3JKNDK`7Fs$A$gvL2DMqZ7!F|5DnL3+_&$|hDcuTjce}*D2 zcE;|dB~N6juqXtrZ~n~#MG(WK=$pi$Av&^sB}_1?lJaNg1=9#1PRqRl%NpVGmkJy8 zO-kngE}QVvWEI?NP-wR<8s0#wtzy{k@=FmvY1|-(#`ydd{=2BDz{D42>{e#sWPBE< zFw)nXTd^1{TGF%OcmK{9H&ixIWTv6C^R3Eh5OG$dpZr$+N!v!%Zwc>z`(^RW5;SZd zaan|8q1xphEQX%-fHJ~%<5!H#>s4doP}AZ2FJ3=WP5nnS&hh#Esa<0>e(vgPH0*`> z#2sE$-$TCJr>GQ<8{A0#py1JPh$RHCN>fN^|AQ5fn@*Xd5k&jItR18 z&A9PpO$W-5)7FhKW){beaLGESzP?_n23M{clOD?oQ!ybZjDBP^^gTZfu5C)0#cq(XK8F+-*RiIkK@fLa9hItk*Yo z(}VKr_MhA2W)A?DF7-$wRr@=Y-*r_NZoh7S&XA;qvLYTr|Cb&kz*A=zkHOoRmNk!F zFFA3*Z}(LI58J`@`P5`+o!vJ)$GO_FwqByg+&Y-O$@p2x&|7HR{mT0zhaCM{)NKl4||SxhzKQb$U8 zI!1Gb2sYKwmQhJPP|l%?Av6+dc7b6FeEA}bPcmXZv9R!anGepSY2}*S*xv3-C@#Gc z1}33`@<5jJD&*not)~9?m0{=MTzJ-I%kiJ~$T2VWSRFP62=pzr8ezPaq_UQ;1-$9e>(buBPu2+C2$0ugc1pd-B>_Ac_OEE1t#Lj$_YNlM z{!?+#ph?F)Y{zX0s2uDe1nXi0cB%*+2VW_wFxTW2lVYqQd7NwE<_g+tc1{}sTcD!h z+jDrkgn_~L_<3RHQuA<1Acj)L(p}+HY*tWsvEkjN4rMRx;QPhAeN?^C_W_BIp+POX z4dO*LElv#}=#Xr_uScg1V@;{I+}ZWreO>rCYCtXzK@v-5cc?*zdMcH{*Ls2Uox$oK zp!V(BOMUXO@0;XbMoS^qUs7hMK0JwacQ#gPN{q7V3<`@dm7}`(kRwn|VI==pOrDWK zt6O-}RvwVW#^4OrTz(VapCSza4w;hpXss+vBx&Gq`M`wU;}U#-O{gcXZfu zaA@flO57We7kT?R6!)I*oy^vG3l@^V#J|#ckeI(XfDFw>Q!Sbsam(|pc4MqQSZG*5 z^m_r`^qqHjyX6d;q)r8~_e*XV{O~4rPOdozc{X?3*%CbcD zVjsUDT@oU0oie?NwGI@C^J3-HZ$tTwMr%4VJczNUH;#Se2rgYOf4J5iN+-vKEzfAO zi*)5P{LEMQ7;Z8oG9yVcHTI%i+fQH5?9c0NjXm%xjIPPs{QoH}y2o#9>A*Xo^`mkG zFU;VZ-c+Npal=g|)rQYKt$j}5u|YuqhYgw1fyF{5M{y3Gm?kxXbBvsW&9y1)P>6l^ z5U4oqGaSh(|0>43bW!TPpLA0Soku;!4UX)g^B>5N${|+~d;jCpn$xSzQG?H-6s5H#&Y)=M^ zn=hC2nEOV*DNz0WJ}(5vvM-?3^9oiOys_8)K@qHqDpBW^5~DVG=eAIwD5af>LmKVh z-x3q8AQq=%wiRi0*qVwKi7m}LySy~2EJIv}ZS?b3`LEG-icd&PtrA0Zmah-a zO4fv;=I6wFVSDH+uk`5dRUTG4iq7`_^kCN_?C_M?lyf@#cFc#*b{-@PqS#D)PJAKt zwx^H$(o5xg^9gxyj#)vpnPC4HCb3g)^X1I%&rPg0&WsT0$*4UvM5EzqB^ZoX?Vc!I zszbezg{{$r{DToMZbQtRU*`C4;{ivMx_5W)_u>5xXWh$-fo#6X6257^gDlC4iQry2 zO>c&`Xx(B{;+@~<$jiA-#?mA33NNc33XwXC11A&@*H>H>yRPzu)We-ST{;?z$Qy$=+v!t6-WxyoK$=c9mD5G4B`bkSG8)riJqsVh4<{Nof2!a{<(WFNhU z1a5i#hVl80IY-(Q@#oOEV}nH9dsWfJU<>b`r04Qlq&f8ezYLq2KUdtzl~o#-;n0&{R6Vr5|Nkk!d_aR8o$vX8)B z69Ey^+7gSkDl3D{!&lC&nbLHkY?7muwU3bI>;?n9BInWrZjQpZKGheRL)P= z?9-dq?|yqB796B(m5O8O)6t7rhTBm8EqXKu4|U8}Z`C*g{>1;YI;?yZDPP(r4;PEP zj}#%$2Sx7GJi87@;_7n{Z3|h!UbKhH&Quj@{8V3QaB9O-s!BUXX2V25{iOT)VX>M1 z{@t%P6`y_I6Z8EvAUx)s7|>RteLTmWw-=N~ayAxe@X(O$=w{sYefWD~6)-;r+b~Brva7WmW!VacSs4&W% z`-Eu+d?k-9@(N7UY!mxEzb^<}dgEj+=F{F~bSgYq;@3?SKIsXxJn22${&M7E5g6*W zn+1y1uKj~IEY2)SK$8HRi}C>b0!&3wzyUC*-zz%9*Jru8{%9i|xG%}m!|>8nGjJ~S zr^*uwHdyzD@h5CmUrfw?eFCMwB(4B#@+qy>WGi%sZ0G5RiXQkbk+z$1dI&C2wwr6V zC^F+F*-anB(8;E|Q}DmU)1mu=P_$NlV=)-y{yUgKieuNf?rZ;-@ioO;C(~-a!ce(m`0PaLz+gT| zdJf=bqhL4(=f-`xy9LRF)uNh)v#=Hog0F=%?}-ey>Q^7LG%CKP`D~kveG%BvW;9uq z=vzSU;L3GyUO(UPINFL=pX22F^*$I%qb?7@Jk=AFme(xM+w=>0VX>xs(nYk!a_koD`T1Z z^G=iL-43y)+BvR6Ye4HnIkf~76>@b|S`N$84t8+?538cQvg7mCtr8>t^;XzC}M{<3@c(X_y5d)K0$n6WGgyMgW2QZXb5hNU=xhrV%L&>K7S}} zRp|ylv~&!Ow;p0+e}AAiE*u(5LoR>1Qmz&)A`MxJ69i`x{tZF;aDUF?y_`-va$6see9P<6wYab2B6qvAt0nUp;R#U)&aInG{zXo*d^I;r^N3L64wya}LV^jmUCx z5PBuunTXK?CBbkN1`?9od~JCI6umW})hsZ7xBa2ItnR6+)}g-o@Y9#O40_g;0-l-S z2P1XlXZgT4Mo>Dx_pYme4pcV9~%4Qfz5SXBPL&0m*U3f7fc zV>sQoXJAMdT#JR$#l}!Oof};?Fv!9G2ZRd*AQM>zM0!C%ffOweXCwuYSAxi#M-WS| z*GT&!NK$;64M>b&qrgaAVzu|XQKIob`r^cKIg30u>KZGpIGb9nq0w0T97dUS@3}7Y z!(K0cx;!8Z;j(>BR{e2Z?|j;&c;sfsib;TAMn)2tXIQIpZiBYQLrWHq|E&9tPuyM6l4-7!iW*0mN(1O7ro`041nBC+wdX zj!5FD)O&((8Kjs|Em>B4UduwFKH;f%o)3&1f>CB>9MbE)#?Q)+?s_1y9*F$};X9trHRQb{hv> z#UdO9_cIip2S6^YqD-V7ZvuO{UfN;H$D|(BrPuj9U=MTA87>`3l{0XnBhV?&aIO?8 zn-_&}M+t?hm|NtCdLAKo!XYJ3Q{C^m}-Y7Yi&e-L=oilGmi70TS0 z=C-KVi!5uVpQCVRiElNW1v{1}JYESvs5(-p-)+Y0Tz2>}6q1qejit_C;koI}OyNS; zT1<%Hm3>olr!}v;v(F2q^O{ zgu1r`d3d7rkOmJWrJw*KRt6?T0S=@U_&*$0iE-tiK_XbKtcnLo9Di)$;&8g(>*{^; zz+U+i7ZjNNP-tHMw#4^EL0q_lKsi`_z83wJl~Q?Y9y{zX^uzGo7svWDX{Kxg^vsm7 zYJPR>?iO*^n5AI<$mv(poAHL3GA}vGYc$m)&umh8**GCe1r}djW^!?8iGN2s@Jf3` zkxW@MLf%;u1VJj>>~v1u{WmCvUWQc=M$aDKq)u8SkDzf0 zf7+LZa~lzsgxOQk=OY^wvDN!1A6`NzlB{pNPYjXQ{tI=5xPUS|J7~ts$D8=R6f1b? zdI%ABvNk5Sa0X^K`3q^Pzx-F>ZHS(j#&j^B{j(Npk89=a?AcZnj3h(MDilB6@zo=- zL_1px4oPSawi}1Ta5??x15fh}%H&U$I||$sDtBv#v==4vb>fArHM`<-h6m$>dwGIi z(tz0;WIL?U8=D3PI`IDyji7c;Yd zKoXH9TI_GSQ??M8kl(j|AP@;ua!QHZuM-+S&&3Fs^=Y_nDN{Vgn35z(ElcRHeTvNn z=&`g9m=Rf7gplM-kO=|}m7128l9%T+c^r%XX4Jy$=0<5kt7`^4=rElA0aADMd z5eZc^<_E zDQT#q2Y7e^UbPZfI&`&YMLYTvTtR?^l>1_oaUo!wIPmi9xTlE;i)AyA!C$IPDv>`U!~`H|7w&5Y(2y=gKT1^f0Fx zju8TM7-y=3gGeO|E6jz%GxX^QSDq_!+V1tH|RR&syt908^EI#6V~ zv@&ppe-`D^ejN^wVfQa&k;O*w>ZblVo`f>BW{ciSr;GF8v`TO}N(fcWU$>xYZY!}~ z5gtVCf$H(=;THl*vPEqNm?`4!S1#6=T$)16=e$$AEn#V zt!+M_1$16AtT>=h-{EeoBtR(~#kG8pHNoHDyyBR3C6k*inDQm0%Ap}|T+{M+j(-^0#I@!U^hfZ7MdV;Z>|{L*I= zLDGEO_J_hYN8kLHj=V{Wv4svO>yRoUad0cNdQaLlA7)I}*RIggVAz%-N@ylrELBTZ zF?kE{ib8cR$WqhH+@6%^AzfCU#LUzfPd7F?r_d*jD(8O;iD0$#vu^Qf?lir2ZLv)}pZR4>=u`uve+d>7{6c&@^#c&_fwqPfW7Kd)aERd>P;3PzQ2 z{7qjO+{q9hNZ{};WuA2^A~sh7)(E1+UX(iVO349*Yr;sM=M8Cc3RTMKjQC&(RB+Vx1uFY z0Bd_Wk?*&AchJ;dM+bV#d5S4Rb~?+!Pq7_BfuuqfCh9(ErtM7}2e|1GL0bFz+h$w600Fe!Md6fVkX<%1N^LA&#_rS>PwN2}O}0zgj6XM6Hrb7U!Vs5lJ<1{<%n;7yX?s3gsx|Gf#7y?xIRFayN~eozEem1mSCoFkq<^fcit0F0))!btNf63p3&4(< z68fH@2ZaE2KJK3!^u+%^BLCerpM!sjuB`8*2z-hUzKP%1iw-!rZEe`KCy1nHCmA({ zL?GH23^&qmo0LD+5>c+Xs{S^8a=iBu+;#IJ)-R5QJ%*boeGM% zw+}yDTfD|}ow77=h8kb@?b=d| za{6HgTd97HYq_<~_NmZkZscIOCxlFu0TSQFZMIl^&})PEQv}$CphG07lB0&VncUui zU+n})S{y>5o4-=Ixxw0=z=Vb~!(r5}(>@LwUCeiNT z?dF5mH>ig6)&`{#uC@24Bov`A^$J-;$9Fq#i=-Q3^(9=b^$dRW3=)V}$iJ@Sv*Bxa zvG=~D-x2xS(G09-s(SMzkbceNHn^^8KW49z^21-Lt|3ksA$?0{-uw6O_o{fvnIl^p z!B!r7V7mK;!m?43y$OcDE6MNJ{c2()KhdXC@5+k? zC4Jxhz?jRysUIZ0Chl|y7nurT2p{)pA@u8E(YtEOoy6;(s0CBZ!i|gTLVkR24Wzc; zVnex!GvS5@#qO|TzM0WV2;6#%^J2_QH4DT=i}|?`L9_Uc$cai}a~%jEx=3$1O==Sd z09?0hTWVYvx9kiv-P*CiVsgjsG8+2T2ilT}O7xdr&=6I3EADv};`-&WjDT0ZOG=@A z<`-hv8bxKJy(f++Mt=(|oSfj6rpHviMMb#!G^F)iibfH&tMEYsRR&?hmS;zaj_OO6_%^3B`6A=Q8D2Hg@?u36<$7? zO%~h_cg0zH_C6X)~F-=8miG~A4Ssnxumz>BD2ZaOtCr-m#a4GoIc-kX{tSkq+2TY;O!$g zXz`g_2>qD8Sy`>x4{`|M<3XZplUSn!i!g+hdM`q zk!}-zmdg{P2BMi5)($RmVI^&gFHS%WKb(dcsXLr$ubF;@G}*b^TdOD@-v2Z1%L|X? z=L=MA)lMP5S1hpPI&!BAg#Ug|V*_|5R4I|hP-ST`4cGI&{`25x(*bMqzb_`1!}!FF zpI<(>*LTAKACvSage1C}L^|E_hNCZNRqC&Iax2uBMtZOYz0tIeqDXO|Q{YTY3qp$n zV2jqmImgcL1!SAoeOyYKUsUc2@S9fx%YD5FIZ+KSaEt}6vXOU9JXy~T{!Q>eBn3H` zlc!_O4&EUT^#0WKi~{gptGwe>c>Rvof(l_Olu}L=mQAfm($+=-I6lE~6T;o3wG3Fj z&WAdS3r?AVESm}$!3aW4Mlew0<9;_)|K<0LAuu0u=M(X29Aa?NC_GUtO?lblEQ{bkr zru^reQmq$P|64)qG-L9^_b9-;}g8pK6OEtB)dm@!(ZrCZcqke)7jYItF@Vq&3 zjLy_DB2aQ!XyDQ>KHf-wu6qJi{-cK&vEvl&{$sYCuOKpKML4P)9|4V}wJ<{wrQ;{u z_i(-bld?!;tUhIZ8k7bO3Xb|Z%j&CfuQXU=N-_0y5LQ|2;H~Fjk32U~&_VT-Fxn4~ zZmv%p^Ww+Apa=b7Ubo>3FpJ+es?vCM8+R@Ij^``#YDiSSjpZujjqH)rxTBHtg4wE2fB>0G&nWfto{v+~t$r?RfV+`#*w(~OL0NYNXGc&w0qG0Z3iz_I~jPQ3sc=pqBf zGr&cf)|J?uaDZkJ$yONJ+eA^)qwP7t3A&ah;J$!at|__ixSKbuk?v4#;tQWy&XTwg zh)Sn*B166zMU3Z>Z7PXw!Z~<5yY_V@6*K4uYL7rH`HGx#qaCQN@)ik>w|riiZz&X; z3V&wRjWCfqjH^9;yewZuB?metS|^HO=C3us;ge3(>g^t?^EqQ1o93Aq3QR&b_wkF# zjA16#P;yC*%nYkwogBTV>Ex)2sZ7*p4#~ITMRm0x(&m$Imy%5|&Y56Zc(pjE4!*90 zS+9B4yACxdzG5&u&ATD#E_*OGj6>6^>u@s)1ZW9(2=9)KPONQe^iK_y9|{fC*F1qD zvL*U)%wz=PM>rF8*iFDrXE&DT3y>^gMm70()5(@OXo$)7V|9Ij{MJav|mt zwomK;0S`~q=xOAJx^wX}iU-2B06_jd%zQWFkr2J^AM4O%lFMy<11>Gib3RgoDS9i$ z3pR2$+OJIOh{xD;+;Qq7&bhpA+3bPR2KIfTj1I(@lQdU#l)l!JPgx`rJa!mBit;;~ zRUv&*GRQr;MdaYG>OjzMVv4z>GtCX|3>@U{1gNy#YHC`yv&aMagZMz`gH#>2FV&9? z?})ivC}f9z6hT21+D0CRRO=uvYP(kX?9u#5p(i05HrLQ%T-I*xkI3f~xZ>CqksXb= zRpY_7g>ybjJZILnVK@8LBrXsuI{Wgctn;&P!;lk8z=WwnC(w>mlUwDy(JVm^EK*3p z22lb;JK?sRb#ClT!Z}e&=xBe45>ZXlrDfH};qXDJ7EKUtw$_RXkSw&N*uQ4y#d#Bljn4S(|^+LE1mdY?l!_ycVg6?bc;K z4~q!J6mu>>(*cyK-@>ZH@-&2PB_@=mMU~p~w|l}#tpx`1q7QxZgyh9k3+0ToTiR8( z{dGF^qu0Hxw)GXu37iYk5NYqr4W|)q8|7DgDf$mI&PB~!*$m@9D3kI_rOqBnxG3{D1vI7cA0QGg8^m`o)U@~{ zy`|u(EW9U_x}hp2GnMn%j!lBn<;3{lKDg`^Y!PJls@PL-R8R`8c}BSr)AQr_>HfM6 z5Hwh!-(!p_j`|ij#3QeDM+yC6DF?^+A;nRZC-x<oU;RU8KJmuhByuyP1C&l)Tu`!gk9s~4ZQp8tSIUgoTYmeU|AE#YQ zAL<(KevgS@;V~six;1*5Zwh05e&p$4dXc~%=yCOBU zdEyFSYuy&eV))ZXs=HVA3oM9yp-`gkvHi~>0XC#OHMwr zgcb=yz}o4-Z+a%5I1!e5zbo7YzOKvr>IJ9GRL}CqDMU@ww)gI-F0LeSsD434U!_J) zM+S-OPf&NNgijCpBoYJ&?ltpgK&aH!7)P7pqTkm z@?fl*o{^fm2A_nwL0xG=fwtbi{UvUP9Ft~rvaFV{9EC`mAu2Ak8unJ5(5sP)D z-jg!VOW2+uIF%aAx@b*YLIKjSroSk!`_~ie{GhX=i?D>{?)0pobhI^J^uYNXQ5)p; z@7k~HYOwEc1Do7&`PFGWA^dUhi*In+?z$Td^2*SO%)}#sM%j=xArp7gr+V+!^*?JH zT-(9`NrjNm*41>2?=hNLW8F(QnE5x4IK@K+P7Q7kMx=?^~1H75U zH5UCoB5Kc)kd!yWt_AA2!cRj}qe9N&Kk+sKZuT_k?|Z#QNLta1QclSI)Tlye>)9Z1 z)x0&ye2xbLz|j&AZIj(!cwC{K$=}ya&5_@wnz2et%al$uPr_VJOs0mKqx&T1{ov(-|b{o6nMM= zjA#iGl|ZVYXW_xWG%%yb$LC33k4`Nw#?yMB7?JHX~r_Ecn!rv{Z`imovBE4nzK|FEaZsJ^$^>D=%@1@wyg z*n-Nu^$B{}%01 z)pHPNz}z0*+Jrth8vmq794Gz8XQprSHcNAN>eB~cSuV1lD;4lL{x5700iEPCB$Bbw zN)fDfKan&T7YA8t;3v9xW;JT9FJA=+oROhzg59XPc*o%l+@m-5%K&mF)pfOH7XplhDTe{E=j(3t&CDp`bHR2~Eup}E-he8UX52$*z zL9a6VwUx@XaC@{P$!eWSvJDakR4sdEJ)e(}M92EA*W+-U@WCf0qv8?K6?huj;JWck z$14QU-v$_=5814z##b;VfH%^b(ua2`Q{;#w&shPR2U6|p)X!=msxuw6*o=XKv7#%(F5eb2Iy-df$qg{0 zN4}A4{z6|zS8eyd5~5p;MM%C_75>#SoS-HvUN`;GN<<+3k~G>Aw68`8H5`nFvxmnn z0(VsC8)su8T0cW2e6Vf2xY?grm9x$W8h8n8w~$hF;X$OW)SkZN!wXGaoNwM|xf+Lz zCNFdh`@e8Pgt#E#USU}fztK{8W8R&MY&j{c=lDo*Q&YQoALZQ}ptnl;^caQP<2*lv zBktYUZ5g%8LY!-znJvft?#-boYDI80BL{rYY$3cG`6;@9`^=+D_mBB=D_WO)J1Rvj zA}e`U!PSgt#wG7xeNyR-J!AE;~Sz8Sp=GQ+8hI*m3pUuxa z&Xh9b!%G%r(MV?1<{QDWs_4NVWM+y%3cJ^mn2x71BMu8g5twGGq9Tn5JA5NV6nXrI z$Jrw&cpgDYD7BRN??q6j@1NX@fC~}&;bOloLB}7Wv{u>^b-9ts_g^A{aN0k+)xzLn%TW*QTEH zjsjDjne3w$qE@?LuiBgGAa4}pQg6?`LP4+f4ZGFgwLYsZ8BJm`#IA4yt*)pC;f>R0 z780>4ik^$7G`IXS@;vzWCgNb*~T%Mm#V)XFDq$AOJHg$-C|=&FWGh z>Ac`(1C#dh6uoHYe3@$MpR8Rk4ZIO6JHkvKEYmJHU7i9DgPQraZP19B)nb+J+kQsQ z#!wUe?HwB=Ja#>jJ&)4mVE!Wdr5L)+5+x2SeGesPN(o)Pa<1BnBt@idrIna+2+qZtte*R-C ziT6>Iwh4R{u_fUomLF5O>&t~3b6)w^kq_+-%K=yN+p+}`CfmqRA+JLG4bwGlo1-4r z{0>oG_>#ZhWe9!Qp1F6_Jf^npAHNs<`=U925Mq$_OY-<<{lEpj8m3;bKuxxK!`&RL z1IpLuRFGN%3!>iUm3yE*179Cxm>d{N6AF@o!H8@W$kZ*6Nuk>HpN$*jd6)wSn%g@T zzw!vi2B_&eQ*p?nwp(g^4d#!Y{nRxzf4n^t0MW-F=Td*u6U?JYDR_2MdFMbfET8*D z)tI!JcsDkd^!Q*5V{N22SDe;t@ZzudN?EHcH-+i!H!g*mqxzZ|l!&Laa@}W>XL(@y zU@Nxy6gC(0T74ifitZKkzWDxBuQrAsCKuubfn!6eB9m3Nv^CwuplWN@NR9d0k?bOT zqWt7qd-B;s*?ZJ(F!ADt)J{Rhh)6xjwd5AijCo5=kxJ&~CU*RO!E@@N@Iv^P{dJ@} zxHJ3bI$Fi#OO$`zLR5x);K)no%$wV9ksZfcZ^iQJc%OdKTtrV`%9Rj96< zk~{tR7x62-zWZ8M3we}uS^(nrEX+L%Sv8?l-4C+I-0b~++w$u3rvs@`tjb3Y)F;O! zVcp!EwuMPT;T`Q+At-rM3?8#7{#VJXI)a*rx+9eOW-1$N2#p5K@zvthc<{l9l$74= zY0`pw#sb&=fA$_y;|eG=w6dGkv%#SZR0bN`Ms*;A8Klp?5^gsWkOwidB4BD`nq}** zygL}|9uD%3zd&jk??a-)qj=NTKhhUdA!C<7SSpBsB%B5#BQ3U8gI@w!jBecojSYqU zMfg2nY<>j5D_IW)WA8dP)u-wQ=^hhCUo_mgKADED{-c&Xq2pR)hN_Aq$}Ia4O(#sw zC0x?$RzK$UTuZ?IlwD_7O9>U_5Huuzce0NVvVA9>c-T7*Os7NKNGRvL` z|LduK|ND1;&;LH>f6jg1o%21XZy(<8*LXf3>8m?JvG(8p>5E6c6+q(C0K+k?gw2z|M1R*zp45pP<`FwUe;;@rDg2HRiW?RlbsuG=393y1~0LcH8lH*X0<$Duvvth`c_Zvx(dJ`(45KkNYfgIJADf zR;?Z1sk_ChcG~_nhi3@&vWmo@44D^fcdA}qu9>D*d>i$nu|>$`o`Jp4i+Q2Gom*>E zu9x*_+kKYAm_W8A}P9Xl@@FigN)#dP8RATD);42u%&wKmAnwd z75Ivxe8I(kh59azqqk&7%q?YE9=bjLe*T)1fMlz?GL4Q$N!G-Q6>;}IE3amLHPB3> zYdl;sF|jV?Plr|PG}TUFlBP>A#eav(gr%1`ab0~RbJ20Um<-c_C0RE{gRRayB;z)- zgN(hvMAL3qa*T`!zK$;qYkh|ypsX07N-Pmzr(k^EP(#)R*&-u7Nb_@wjCubPp=2f0@|q|f`@?2v1oi~DWg z@@8(omCc%FA6dq%KE*>~RvK-y>}y`Hw>eYFd(izY!vyVJQ}J*I-tYs)uTC6L|5Q|@ zQ0M;e;KRoTi8P*;&)<)nIC=O;xyCO zG`6ID4?mS)DUrC6Rlu3M@0)&`O5>u=_LJK)7VKE24BrYx_%J3JyjD6@`f+-aen(so zL&s~)H4B&KZcQ}PxJWlt*!(oSEi||Kx5i6$C4IH!tM9#(S3iC02w%oI`gIStmK^Wc z6Ex8gp};tre86z1Le?dOhUu}s@1y7PW#_(E-TE9}IIZ*W@r@V59W5u9y~uj|#L3R+ ze#4xdrRmA<#`4RX^S)M8xk#;is`>544y#?&uiT?go$D=QrKzgxAO9)(R|a;@_D1AjenWsM zyqIC(akbp6?D;Pnu2*aI({I21I7u}(YWt$`m_n9;+^xC!z1}iW+rN9Ha$j^5-qTXO zjREyn#^4UN%eLiFC+7E5d)mczu1V;7e*Ey0YkQ^&+FQhwLci>n>HF$1I~p7FY|V$2 zXR?Rj&J34RU(CxlUvQhR>D#nbt&uhDBb)&yhE0A4eTU??smRll1Yj8085%BPA<7>=fNF3(UP{_g#2i=XT*; ze8NYL3p;aohv7x!z5U=+&9Lg@KdDDU4%hxxX4H)jkSofx5Ot!yaFx3E;%s)yNsF?y z@o~SRi_tSCQ623x_us8Rz4E;x^5bNFqWN1Ekuc4tX0AQdObdRCBU@UtIjM!GDb@Q1 z7|+`rPrh`Yert5i;g(F>+wOl(7LFN+xkW!@R?ya!SfiJ_(0kZoXqN5V-k!lXmuvTA ztRAQiwR4|k)p!1o{J|w|e&&LB;2?!ZVEw1VQhF+~DG#Tv#vG`+9w}PXvQ_Mk_B)=V zhi)Hh574Xx7t6$ldFRu*Eo@Zrs z!fh9$?7#VPdQd1LCa>A({a2!cx$)!)Cxm*L7ryREG03JSbI@Vzck}!z3BA`!ojIOa zDmP`=9|ztNqt0I#%&Tj9mbUBj?@v)jjjUfJS+A(Fdi|l_cG1GD|H<#dINOOtpIh-9 zLmC3NPn0-M#NV2eI$SHAE{Dlbm(6<%_A+-#jH(`Cc=^LvK2>N_;>iv{d!5{w5SIgV zjkbnb?vyCcw|S1{#Rpxg!Ac|3uer%Xd7tYo63lt1w|-lkQ=Rx$maP)2oQq(~ z`9<=R$}(YTPyM_##kH{<7DJO9L(;uNwi&kz(yC_rnEdw5b9(&T6LQUHE_!RTf5L7& zrC|572Q-R8OGe6T4P`YR-oN?C&*YZV*qVfNZ@s?ko7@_$CBG)0)Wr$ZJmN6*ucr>! zvpp)|{qiXzf#sAJNg{2osmJ_fE!wDK9PUjHHCjsEs<9vw>JPlrVG`C&*^iefAt3>) z4_>ctrlzKSpRszjsybTqS#!o~p4`>zb5PYExyyYMc3#rqYirmjYroJ$DP* z|3HPygjeXan}fC)$FuV;QF<+@(c7848DvWea)TaUJa*ympZP(%$9e1$0%D;(Dr>o; zu&+E67{Z)6p)r#aawk4TeiIEau zm!W%g`{KZw1W``Gxj*Xn8J}Lt=E_cAmH7B#$>PGC`dFX&-m^!38F6J_R#Iwpu;83y zx|ft;_#a*YSO4(piRQ^;sblu%VvB0)A!|y*B7rC>tvwhUI?v!DoNbKBc zdiwM#R@Socq_OdF?r;_EUFL2DiJ_zQI-y&a_gb$kX!n!cMkgGkMlCt68}+8B@U+dJ z^PO>FU7_-t8Iu-!vYKNQm=^@YVq0RW-tC>~eOu=4T=2+Z_tYh$D>C73Z~lDWd*pYA z;P(o}KF(WTmecN!tUaY{cy!Fh=uY^e?VUJfKLtE~#S1lsLCx11YWLofJ5a_cG=5P; zayD1`ecdV1NRIoAY%WCxFQaV@wo6QV7?07{GrD>Bmu`?-d}U%|%W?C!?eUVIu4)HU z54$|Fes@_}$COo$&s)&p3J;}v-hf$VUsKrB`<^q%G3!3Xdt4nTK`svG4 z2YULJB|hHCC$(Yg#z`@ouA2e(zMP=f>hNk#UC?>M!`x%p@mf}HenQJ_qUkVZ(^UR5 z@oW8j*CsbCJ9}43YS3JGd6+)M&dyG7O25mDO?#Khm)4Dx$TZs(o_aEGD|@;>uW(8n z@7iV1S!%RPv`xqSeP)b#R`c>fp(|Y>pE?AP86~!5oX)zONVR#y&b&7)D4nbK147(B ziGm|Jf+Z%b)IEQ~?B7=nT*afgtMTyY<%cVK=>qHA)<@SSyc^e>HnCM*w57DZWTpOc z>EPn8RMEb~x9WaEn|pLNJh7eHt#v5LugjlDeO0rM{B8Gv!wGMF!xwmOs2MAy-&={T zT>QDnM!YIW>+uxFb{f@m&!eU-CSCB<*+K@&qjM()=LI9BG&2pJ3touG=40P$EHN#~ zxjED-_np+#+@%LOqRJ`_O`U&!89oYnQ*nPYBe`!~g-;RD!OFFVANq)|wXzI!x!|xe zw@#zsXt2wW!w37@2aRuV2hOrwOWzP@_FjDcXhFm}`?B}zZ!X?eK)o1oj`eoe7qyJb26o0o}W1Gw;LZ2VLBWbR;f78JEW4ZCvXMc_; z9yxvM`%?u=tHJqs=2@!*(O(DOCbCz{^j-9F{aRg~ z0=0Y2=jCTi{QkJ|%=*J_ZNz(#g(U#i@f28YjPXiGCMPpV{u+) z)GoI(_n1mO`|;m)CErEX^O))EW(^H|o+o~W1w^O?T4#wM`(>~>D$NBa`ZaQo%H8L z3KkJrlOf@|&Yzs}vhnT8IQMCz=AbXlp=R%*P0u=A8~5q#V#_oP%~0^&$XN-r!5)@O z+Qt3J{5U5bUm!K*dMh&Ixp>}gmz}J2)6edjtm5qI5`52pnj=`^yzKqpalUgxcMPt!M`$Ul&2|gbxsE8$nTZ_L@dy@n z;cctky%??<(I~m2(af_j)lW)g_Ka@LK?PT}ACEE|Dy2k>>>uuZsJ7Mj=(ur)jXc}S z)Ybb9YH2U2ej52==(h1e6;H3XtPnr{D!6iPS2%bOJ@<*$&!5eJg8XoDak(D$vC<<_ zMR>y2)TjN;&y1eq26>`?Gz8@idd;j`e@rQSAA_I?jhj6eqj03y$r$cT9fg<-VSAAR zMbkCY(jL181E0nC3fRpYoYZJy?b5BB^+|nxsK`?&CdjJv*3l&A0M%L5to%(W#)*^T){P@9`+S4uUCktIB zX)!vP4EjPmxF>O%8h_O}ro$^FkMGXxl+nAg+aTK6ri@W~eAn_EtucGy13?MpZi>j` zx-l#{^y9SQw|wgjzqzz#HLofYsry`4Ws>V(z4@p8Gb`0_Q|;wFvn5-q-5=;G`1mCS zpYRM{v3z+3hJUSNl}f3e&+$v5`3vF3tI>Gs*YK^i3*{|7dcjU@?KN+;^%vxhfBV!h ztYB1Xzy)YGOzLz;U~rM$rrX(Smw~OJqgB-mZ1dB|b z&eBNJ(VgJgkSZXA+V|36%XZ>g3q#xOQa6-0%WkXRGIm5!ae}Y2(Isa2nXinmVRemZ zg2f^73x4VBn#SIM`!Kzfj2%b22l3{8;G?jT&wQ@gh*|dX)Cnl!Zdw#*M zwl;;2xPM}(?+Qw_g@nafdBb04fa2m?L!GmEdB~q6(|VQC7Y5rIs8>;n%gRKia(acCF2UM@#-9iksnB zFmrvanV|j>wYy<%BAtF|>Bkgz-NgHP?LOzYSfmlF1cw`z($Z221-469b8>S2$5+-m z$vhxy6W&%ooK=>tHHIawt;ZWfT=7Sv|K7j9>F{hd{rl13e#)Ew{zrZ@dOSn?(f0rU z`C}qW1uwZ+^%20g5bgq`?l)7 z!TWP=OP9u@rJWp^fgx%!2fS93_NT~Wo}jR7yX`2%CWsgf(-y&f?S*Xz2Pm>DsGGZf z{=5l?D)Rc2j2hZ{H!WTJ{Un1)Z+?D$Q}{pJlRS4P?L6!bFt>WeR`tl6u=pyeO^T}B zwr$%L{MzWB@dk6)^HHv12o|sWl2w-y4B7CGwSH#TyUYE38YjrL<+Qt~iY;3~xvzHP zY^3vQDk>^Wl=~1qj0kk@+_`)EoU@Go;MZDv9QYP$YzncGpN7D{}^^r*I-jwAQ#1wI% zTzkgJO6cX2+1499=By8Vf1QD>Jg8_UQst0G`^q20+g^p|jD6Tt5fi}8qk zo;-QdZV&fgct}}FvS8~Jn%}>FAMU@RqknqjJb~XLTm}r;TN&(O5;O8qT9(~4pjme> z!t$=Fv>9z{WBSB?A^I;3Zs!k$z%)6!rCqk@P9r_vp9Qa?`T1ChJ9phN2PXh{9IkH!f@I6%JP+v(Fd8@4i@i zw{**zcTlO~cN-#Q?Qs+E!S2GqKv9Xb%fs!ItI#L(g>zyJ8l*B|B>MKzJw0+y{K%gK zkt7}ZyLa#2kaPdd1)no?cQ_y`B=r*akH3_n75BsNf69I!*X^#uvY51KHqu#=uOF`B z13yzT8}hWvzxCyusC%)r2LwHgqG#sj%tyN`R#(Z-e&2-npgDa76FJFXWlYBN#+vyC z%yVRVn(Td8puT&K)^gjE<{blPnjInnKk(HGtqY2vz zoHxOR@&=Cb;dGDH$UYcc-(|Ob8x=xFt?~Xby2m{4VOWTvp%~_V`<;h~wGMGzjwgQLC zBQ4F0iG!j!iZ}oL9+)0*z01&e>~qQFAuee=cN?K>=fOa$&M@E)|2_XM;}V*IJhUV zAY9$sd3J-GVb*2@zHkrszv)9q&2lXIjoOVy?Kh1HN#DPJ_r}GV!Nn=u>3Oj zt3uopp8u>pxmE2RF%dowZ{2t4Z{9rc$3sl=+bYY^FE4grDl?UK;QSGA$gVCTVO~84 zo=qf#?ZF3jqW8p5y{m{WC8L3FI0AH#vLz~YRgn9~F6cRrJ=IDvuL1ZBgXFf+&dr`xiH3R-{<=_Z`? z{!goO?-+lJC1+j}5(ME}$-QGoMyt)nn}rvm>+99XYq7g@NeN>_9eLQ#$?y7?(Ue&ui?X8^6?`xve}wbGaVC{vq&xvMweZkol&msC`d?u zAbIZHO4!;TYRP`OC%gyyJ=b$l4jI(RN!X%xZUY6*l#S2z#8TjOu{hh{c{A;x^C@+b zwKW(UpH}gDDDTMHkmh^*>>mx++-PlWO`2E$^$$ui!@?L~I^I9Y zKs<`jvdIyu>`$-!;(^euM;l>_p{S%ZU`E}${TDbC7?mm@*Nnnpue7Ra4QvTNyt}lZM!?traM=MOfZPiNUX6j96vBS9EbE;5^@U(vfByU# z8LJaBFTT1g;be~JrQvp?XM=JcL$C-vg-RM*dKm=>8F2|EUg^dz{ro6-*O7_4dM561 zuI;Fv8i9*G+T&E)`H1*DLrmioLbLdA0Ut|<2tcI7MGQl8O>&AW!nSEOChLjafh=Jq+)9EGW)0huG*E`6{_C9Md{t!+umQ{X+?(G3TRg zvKc#!g(}MZHxZY}^2*AANgkU|S*!B$@<^eOc7AbT-laiZMMVL{2ByaPV!Z4i`^o2L z*0($NvZom4(Nji{G)%ineKt#2tc7XzxiM?ufNhh`+OR0W;lCSTw70OSs;eKiv)jSO z#%4D=b{ZlD3WavdmUFiq{(NWhG59F#hb>4`uDyBNYj@Uct7ZkL%OYKe zWG05bvclBbJ0v8;m_5k7BirgLHHC~jUdO)YL&{6>Rj#EZzt$dk7{1cR#qItAQ82M}u@9V1G&eg#!g#&10B* zhlkUpKMocx&Wlkx;gKYj7tAmIQZ7X3=PW$MckX_9>6d}x;lt+eG09h4OF^h0GP%JD zmT{JY2M;11_@}@AtrFozDTYIO=Z_z8s!VEmqLiO)d7IqlCY0bSn&G+VVYGZTMcTHV z%*vR!vx%9R8HIjwtCYABicmUw`sGnv*Ph6^?-^<@46KU~GsnuNQ2Z6IzBJEX+M$N;!T);YCIAFnaf2 z&nk5aCJlf$cDqiAAgaV)_7e6kb{#JCg+T?nhZwlEWW0Q-D53Aqd1mLb2v@(X&}}`LlY6#QgZL(3?6F9=g%xyySp8}ul1(Q z_e5mHccs*PT)%#O1s?Rs)ZXFFGJe%{!FcVPVWo2rC5t#H+m&n9#3F6nQh77z)!!KH z)&8wC@;#ye^9c`xy61zA7+Z$H*nMxDR$K-2jnvs3710>b6N4y^XtBa{2Ukds2X??}%DXVNYpASj~K`1$kagv%)@ z+xD9kfz0L@LBCnvqae)<@A*o9Ttrl})#TgR8MBT1OW0nF<#+AMCQD+Oa8izlurSRr zIOR0s-tVCtz-t0Y##;Uvrmil9YI$k8$v(8pKb`n=h=UVV8GOMBJVTpSKiw{5*!uLAQtY;uzc@S#TUuK9@5+DPQ28NIP)0`PxKQ{Z4lSYZ)x!c> z?U%PL4sF!a4zWD+xAoFidOIFc)>4_T)^K$83=@L-8m@rdLf0U$O4xP5N2Yma4N`yC zz(4>BK^BZ>nVI@xORP@|A9i5Kepz#~j#$^St7v((4P-e_A9HtebL;HvyxP|XB7NDi zWvw~3oG6hx`z(JK6jaU5&3!{FOdLC%YLqq9@`eyJcc3XL_%l`q3)x}>-qRVyM-Xjb zo3xCA)|)uU8GFp0-Zww-o>&oIs3@+IP6`eR+I6J9R`r~dkP6BUvNw>qi(6WB_3+?G zg-w3`beg)_`E`^i#F5i-hfiqN@xd*+3tes%=9|-P-kiW{@;kx2DmXk>MGXU?8j;h@ zLaZfO2nctGvfPq+?$=sng?;=a-D)6F(&B)O{Z$`R~%Dse6+Bhn~WF85WLd>iR1gGpkF=%un7>$eulnQzgGk6E`x zVkIoy^@}_T+cn4Vx8uppXSOHU4}9N-UtwJrpJLf8N<}V%6#U5EUHIHmxX1^nl}fS% zT@V?L^_cr5O=M}T(icR;sp{(L%HS2!6sO8;B(=~dj`BVxfKSV%*u0(l>Eal7R>%eC zTy_1%-GWaZo;)|_Eo+7BtDonzL5q<#wsUAGSY3Yp z!&4WyN-=V}PtcK}-zX-cFolh1E@5Xff||X*t;T{f>?=N@(RNmhA;H$DYUqVYZ2Fdx)<1mv^xjdVvjY&S>V?j7!9 zKbdiA8El$-$kss;0(7IQ4DTXjDyTQj(kd*Z&V zeU4`i!m=l4Y8x8h6J3Rsp8-H9 zHgX)TP82H7D?h^ZMt($?+})h4nRv?DI?#E%CtQ{OZrG#iTWkvgb?TVPn&bL(uGGGd zV-;U}-ZD2?cFTW)8V^eI4lXP#-Lex7w5D9jwF|J z>?R#{k_=Lv4;~Qd-TFF;JazPZg=z|2r*ENiYtT(exik3j-V*RgjNL!y*xF|J?ziWo zr_$8YqGM$ZMDr(;KI6mx=@BE-TpvBSYpftikPV%~)IKq7(jm_V8c?!h zuzR(}4>WVuYVspeMl41v6p%)jv8t9=4mnExW>z=R$YJW|{J`OCo97gX1TUil|e{q5DO zTBqJyN>*3bZkw__<%Dcgg64qT))h%ah$iKZ=&4^v#=~h0!qzJ9l{H){Rts|uwPvnBW6dvGO|V2F&_PvA03b>y7SU#Lvz2Ij1EF{0g~$p6*{;J4 z!DD_>Z?}w$PCX4DSz<8)2HC2mAFOt13o%koVVnyk$~C3Qa$Pqg^(w@x_%DYF1bxJh zK9Fn*4?PNI`?<(dz8&BLtk9Kmf{sU{9*`Ur=yuED{;TeoOQf9H@_@W*6_2BZ1EmbmWYTE4UAZl%)!x!_(2 zK5`?58lriiga5}artJpasEtk0rH_n{mm$VEj-o_^grhazDY+yMTOtTZwiycA>Njti z_fDaGAoj+Vs48xM{F-aeOUzl2_Q;u~%>c5XLJV>gVVT3Jqsbne;W_Gh#z7jmEF zD^@r)$bj#v3}z2R4Ejd=Y1<25wTlB_h3#=#`5-)fyWod0gOzPh8bOGg6^z9+bWHy} z1;2*s!y^JYnUKzbX_7tl$?Dpq?|a%mH8$mEzw6nr=ltF2wXre7^dN~VR@p53574is zAY|m{BGwq@+ATw{Qbba<>nvXORUAGkD9xfF#>~hPk}a7S8KF?T+9hKiT$Nqo5l&A` zELL#ypFH`j_e{cz>YANw9)gr|z8|suubS*$h|fu~^YxC+{%>!|v^ zP4vG<-==`DdCJBn2w~_H$^!~Dy#ySxurgzSs}j}^FKs1dH%xXM92^>9tgM+1pjH+< z%BuA&lKWBS*G*>A>a6+KHVDe7dU@Ap@telg)NHD*d9PiaR~G}{9LiPHCMcmwP%RK! z9cW3>5kUSy$H5W&(&mc-+9rG zki^L{_e8Pym(h`GG(j8K-_?bYV!{#B)YRQHSdGxw zS%Lv>27@`?M@mUj2)uhh;*6?J@;5I~JB?5=gJFQr;ey9}iacawz^U=U`! z{FIfnK+2W&Sie{uQo>?36pE+Vv=l&Fkbe||MeOYAS_beCP)=Vk^IMw1=49p%R!+AM z(gkW=c?|6BZbu+j($?KK>y72L;2s&0^*6L}zQ8%&C?)#9XcbaB}O1}iN`VDs@? z-c;OBG*AOol(hO#oiKO?Cx3qBbgZTj9PLcehM0k81M*)-+8w5^Jlxh)273^7cQh-kkjWz(s=4`Lnyml#|#t5C^4=t{EZN~Ir$G~r0T|CJ~*}H(k zb8>t`LN=a1e_qAy0(uaqhV|>#N$y^NIW#x0NI+k4#>|47yZZ(QX#@n~vkDk^0*jXb zYQG6x0ZAW4o^7LfsC^h6jCfQlW&SrgK?^c1GDu0QxtS|QD5}YlD>S^r*yHcEym`vp zd}9l(>%V;c)RH9l-VvAGD1c^qm*>EIj>3 zJiYs@9^14(RTr*jbQdmM_;JZ-YKhC-asZ?wnhO9sUJB6^iiO1wZTnOa*1t?rSc?bt zCVoD_=#|u8!r*VfQo3`5&+hW&kFu{Fc~>ticSTXPWyD4J({Jc2yC8an716c zL(;artuq&8Kh}DH8?uK}6Ue66bjD2>4brL)xoj*!m(zBVL{F!24&B2Pu&0e*a%>L+ zi5_ap^8yuRij=b29+8&hS>QN8ozgF#2Rx_5Z|d*wk1j+cm{UF&#e8>dxG-5bgjI!L zI_!%z$*4R$LfXj4)zwEx(?a&(g#ssKk|5B1MD!c*D&0hTAqkZ{@iInF8ckR!v`|=k zyKBha1HfjKee&M7=M8cP5prvtx=(6s;FP;bFne6&$k^C&3JQuYuZ?oWC_zRcO4R9_ zBoWT9uLj?}Lo4ptvu;%QIDm0adG6Am+1@9Vt2oI!5H3GnzY~!m|J%PpDe?3A!;;Nd z`SvKj__l5Zjl**A#yT=Ha8g%Ps20^GW(Rim#uu>!pp?Ne4>*u!(?5BF8QmZWzJB{2 zrcK$eVrB5j*w6pn`<=JK9%*j-nB@Prv;+~!@gEeWb8YfG^+igMlDT$4#a2^HD&%7Nh2iswJ><-gOd=jDTx zye4j{eVCjS7iu>P&FFT7xh};H1U7x>4o*(a?>~NUZ{NNi=#@(qJw5#h0PXNA>~We@ zahm5meCoIa_R`3FI)OR0rOk1=;U7vcEu8SFAXoSOM@2mzc6n~g`xUy?-ON2#49(1V zu3qQ%;Xem0Acc#EMP)(+Mk|g z7~6CTttT>JLE3pBpLULpz~>U$We9Zaqi_watG3~fze~$s2bE$ZfN2o)jaJ!|tKe?Q zFH@G_!K(OEkA9%3r(7WC zFHo)m=Om~qkjm0epSFOxOgv|Lc{Nlr_(% zuvVjAZ+i)=Z9()$^U0Y^2(Je@fD(1gK(;}$E_@XcCfL4J1|7uzCL`lD6L;8k?k6t` z)>hhcf;)8^*2iT`B_N>K3?J-6csK0zjPQ0pHqX6 z@M|S%6Y428Hugf?5uw&obI3LL-0N6)iJ6EdcqZGO>W;z_y{IebL=#LyZvLk8z&Ye5$d*8By42Alju+7Z+`p=)|4U0V3 zv7@#dybzQ^3%UWF3W{g1)KnY)UP=BI`a_2fVGX{CG8B#A1zlF%OBN+|JVLLBhX;RT zM_B6vaykURm!OxvY<<%j93-UTDN_wrD2Ny#!CPyhm6QTf0@eTMI=d?gyIIL#6yP~Z;ymAd`1 z(OiM3@G4Ng`*?YISvE#p19!&+bc6}zfAT<*&X#Z${+5&zyrlMqvP6X9w~B^|ft8b$ zRTwx5|Ic5)4&iP3q^714?ZWXy?K2eyNq_#hWdfvcdUll6)Y@8@Me4jD@~6Maw~UFm&% z*;_YxmvGA+RK-UG8K?=l)jvN7$m{kf2upgry3&RQ_378LcAZy0W!}92{X@*@y1UpQ zCs2xL!_ztdTo_f5QY>@CojW`L`J(5}fByVAfcxMLKjdq#ix23nYkp4;+m0-_4kX5V zEAr4u+*d=c>9642<9GxW(-B~I{xG*CPm>&*{GNBzKidMj*-a}}ucLLyGbG1qvAU>1 z&SpCHYkC!(c-FQ_PI)>+{?CXPsKGQKK@&bRIrFLdpk%ss=;Woa;XKl2DzfBcEX4fuXmv%$|LX+}xox9?wv)f*4kSnZNC5-ns-JfCKwQ+7@?t6Sj$~nRtCtC87Qg9#gsT! zNy}RaCPl9`GSPi8Qpyp9eP~z7R~C%b3YP+7H_UU0tH;a4l2(X5?uR?|#|p*sf%?V_ ziW8o_drykJM8ia$Dvywm3NFV;JKb2895eA6e?WioJz~&W3VD`GF~0Qe+hYYY-GMw} zV(M#0M+#p8=#{84$im|78XgvNb9b-67V@T5Kr^0c*~GJKKMcf`#M;ALO9g%`*ke&a z2j&N9=85So?XNDUBPVn}zTDmvm?}E$tyU_sUsv}oS=nePn}tJyf*9%j6$|EuUg>B* zANU3)4@y}a=%lBV<6!k7 zn>W{i=jS=|_R$HepXeu7QpmZR!ZEPRYbj>PVW$I9{qbM7QTzZt=%oLtU^ZdEC$BCv z2LnqFcDs3SQXC7(sNjkQg%;lPFOBoCWOa3QmxMzST0>M%p3YUF#-fIGvKQykXU853 zeRs$+Ej@2`cKgJFyU>#h6(_$W+2quTO0B;Z!R(Kfr zt~c!rnweS#>lzTqJ1nYcy&Qo4P*#~gp*el}bQfx*zUJhJ_5x8bU=TTVqS!lf^yn^` zm(K!1Le@|UXLkY$rd8PN7K|9YE>1IXT1JZh|2$ z*hbuGe%644dNp7Da>GIwM#}Lb4|gwf1lLCP_--To3nns&->W})#cRrv75iutB*|11 zqAoT11YQHFC(wN(H8(2TU7_aI0XW)Bc$%sZ&MIQTIobMWbQg(VZjG*I0RDAQgB*E`Hm%s?vrLJ zbE3-1$|#REss(2|3LJYxp#VwZBm@97m9(2Tua%dVN879_HiZftxs+%FjPuZMA%U!G z_o}RHkp!v;!H@(LlPNj5%8(pD19R%b+4sn~y%1d@u9zZ=%>XsB``Ie~2QKqC+PMQz zv=`pMNoMO>*{)E>MN8l~(8+j>m1cp4?5jy?p0iNNgS`42pG0vNDZjNuE~!khjFPR zz|00J!dgT_iq-@LWjp>OC(krQ1_8=pVioY-@8XiLF)XZw5(YJ67w#LXn%8O4B0G0( zB0^xaAWrsw*qBo@dR+iLk`g{9CgxUHm^Zpf({evL(&dOsp|;+zVZ)&`eZ3F8t*xyO zwgN%WOk@B@)*{^NnsEhjy~3MnQw9RzO-^|Za`Xs=T?Q1Eh2i!`^90=P3~{3Pu*YI2 z@|y5P$W8sL?G>u$0olp z6KfptT70;qbm@s;mog*G_@WyGc*Fx0E0WLq!BOE)!*(IO)PpDXy$s zjV@{##ck@-S(nU1*;L0TmYxBhloS)BU5BA)S?{uJmP5qg*s%?f9>351>?kscz?qA! zW{`W5-V>*@9W(zWctlt4z(ATv_o)-aO9X=7Dll?UgNa!$U$#`}Mz|_$HDxM(|9;c{ zlGe^Dem=h9H*ZqQ3)K>$)(2V_m3}+zpSrEPyBh$Y`cw3w;3A8))Meu?7rdD6j1|Y7 zDFW^wf)zmUnJ6laoX`Cln{jPy)mL3YaS{3NiJPxRpt%8^Ll;JP;Kr|_tw#p8gBIPW zz}MHeX=%m8fQ|rozaDF{F+6a|9ZC}5lfVlblJgTSuOG9n`$$IJC=p8<;XAe8V zN1*MdBpqR*AjsGM7w}O*At`fVHVt8+FxQ|Hq(8io2ZEWC{$oUR&>OUfvbhPpgLyn453E9~87c(q&*90no9yKg_?f>sk(!M%=zzQRL)H zHRg^qqrHatj#LJzy2JY_+#v=?^~d7FOE5iqHeT5JYyPgMn&&c~yaBRTf$D)v`%UXa9ox07NwiRGINh)ZgyuOhnwr-uslsiAT zHB{cA=G(twpHUb@8b@#4jI;XuB5{erxzm9-zx*obM^(m_6c<4o|KGUHwUln4B=3cx_6SSRNxs|e(EP02 zAfMER_(6#GxB#3On*9_`FW@1?zoFryG!bs@0}!_Of&CAYX}Kf&n&CpU&6k%{lx~nu zAH$JfiOf*kvXQH>71aw3r5h`f6bJz)V_w_!FJd56f9(r{R7^s9PJHG6ey2pZiq3cI zPX*4;DO}`ay?S~9XirG;4(D8cK&xQ*+U*1|V)(;%lY#`e8L7O9ET(V_&?_~Cz!Ujd zh$NU%Q;tGy!mPt(a6312taI1yvz*idz^X?PwC^8ZoGV(~L{^=58iW~CNEl6cA>>0# z=kg%aK-T2tJxF>4^ws_057BfzZIcIT>2>rzjw36gQL2C{W}-IvZ^$1-i%x}vD-JQb z{r76ROz^fi2C$vrB;Sv9st2Gi2EMX(6fyy7N_Sj@F=2}Ub16WmJ5T|7+z65+e?%)y zHhB-jg2|SXq^?Bi$@1tUl2Z8N$J>z$#ZVj~U5GjU(wz~LkkAxcKpHHC2oN;MF_Hw$ zNSA{RPb5(;HFpy;@BYO5V1|Z(87wpdj%hKuPW_|<7nAm{-`dft?F_Jvd>ih;hlF$T`3cilbx<(XT8jxmD-4uOp zFzdoo!yRHhZyyVj6$2V*orfUQUP*TL_H?B92l>*Cs+kTH9UbnnMfIf`5*v~20$JSsJa~K!9-4EDZ;m2!R>c{yn<*%;70jBjhzJ~++&v) z-~(J(TjfR33c$fr7gARfQVp=7XIqEL42KH>v0=ioYDoc|V{2k+W3wG<4neGP5s!u0 zvvdp$d<@h+U<^Zy3${&rtJlt(tEt_DUc##tOLMng3rx?9r!pc2@^FM%w$uNwY ztkP-K%G0Fy{2r=q2g(B7_&z|SMB;M0j0P>SBPj!ubdv2bU;Izt^mA8#@nE*6CK05G(aryfy|q0#|~N;w~L8O;ZO)+a!^(K;+!*?ab){?_E~$u z<1G9zfF3X>->{?(K!^mMkqodZKyrtKM%qvMN92fdp64G_$0NXTzo7sCgkFZI7YNe# zVD$0L<+{EkCI1De!u@D2E)m&u|7VU4Eh*u3%oWxyDg7T1N`NkW)6%y!>!ZpA#hVE= z(trPVT*!YOR@xOnTbi)jyjTBR_GgJI%WMODTH&587v;=Nyb?2^-Id)z6!L));O?-M z*>Nla-6(*WN>l=N<2^?Ua}m;s$O-aBl*QPJB@mnwmm^wf7~3GjV>6Y3vqqD(2VIM! zI251dpur&jK@8P^g1-LoUS{Yd^Y4)bf=O~X*25?FyN?%<62BXZ`f&Hfo|`3ugcZ!c zJiGx%anQ<7#UY_ue`C5GcDp&MR|D?uya}kqC&{MJj{U& z>O&MZ{gd=`8oRb)i-;J4f35JN<5juoOb{qsI69VQ;Ub400TeE1@&&zVyP;A-(X0(x zIlA>ChIAd&KXJkzTc>agWSkm>7LCOwg|(SDnXzoY+C!F{MoDNV79o#AIY5&OW@ZcGfGJpJIKm%lZbg4LRe5 zM-62iIY$%KC5v7*WbVb@WBwm6VyhzquCM*p-R)S#iK0ryeLcXCDIj_gG3#V;q1uu6zbhsja}02`$cF?k`m_JuD3E~8up zkn_+GqA-^(^ygAXC;-b&bdLwP{X|hW?g9ed9N~ zjKfM)Ol8=?giM6IhF)Rq%`H1uLz|qA<}wBq0T~a9b68gaW|=|lO~k%VlXpP+zI=H@ zJ898L`1TRVCe{lJ3me@B2^`EOvs_(WooMaZ_c-oXP*{$ev!u1IALQqW8Bpc%rqwZ` zS|So-1pW!`{rrUJ{$IgP`CnexFM)n(MdK%v6#t8r_`mjJ6N@{$`HP;5#1v8RNBPh( K#WaOe*Z&`|+J0>S literal 0 HcmV?d00001 diff --git a/bench/time-tps.png b/bench/time-tps.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c033ddaebceaa03ebad6771a4cc64f06313cce GIT binary patch literal 56726 zcmc$_bySt%*DbnfBsUV$tss)poze}`A>G{|-5`yqbV_%3mw=$soq}|C+-Lv3bH4AK zzwa3L{^4*S+xLA~&suZMIoBdmSy2iTjTj9Afndr=OQ=F12u2VHTrny#_!mTap>gmR zpX)m3m*%M-G6_A z*}>U@#d#j#8a(BhqqMdQ1OnZI{ee?X-uVcDEWyi2h^l+0?`L>usm+iKc5j{SB&H z&wqUo_W73-If{h;^Vvp9Bw$sa@bjQ;b?|NK*8oHE^~|9*|~Db8+)%6~se zR!A&^^?yz=EcKt4M&WG?#PpWDFGXu1!IRBYnLRw|e7r?J`#bh#LTItg3)j%^=4Gak zuTJ(u6`k65`R?3z(cq)W0_%N|xP;vHq;)!Wq8_1%Qmi3R)rpL3(sRN?(X(>{fnXB@Oui`FU&E&3*9-R6`K})c81+kO`vn$uBM_Y|L!_HBb?zmn@ug$VsYWbWl>@Vcczey?JbNLAo z5%Fs~T`H%Iq_K&Kc7yg(5=L+QOr;^oK0X?f)ly4+r2bUNdl_|gyi$sWS=CWB^~00L zhr6V=WCzt)39{aLowm;5g$5~sn6 z-yPG*`8lm~vw-2a+%SdbrQ;?&UexQAk7t3KlBX@Pzwl<9?aK^2P0!bq0oRvhZSS+Ws~z3TF2cX zzwK-l9AtHSJN#mACXH0QbjtYt`owa+?)8t)Z!9dc1bw)EW(miEd@$Lct%gQQ%r-gE zz^?vRo|MVc;{$I^!K|WD^B@fkO|`{1`TcP(_Nrslz%EL?PK?jhAr(D6s-X8d>mAt- zdD!twHCZ&$)EUM~nfW*p5)$G^wssa8S*)i@kg&0_+q^H(NVx6$mfL-*&Bus8o{e!s zerNiFPzTGu5|1MnoTxUDLPS9^0g0$p^#LxG+aVStIl(dh7L#~5RzEmowaFkB1_@VL zcB^67VI-~kjA^gpDhr)y=!Q3#V4$bRl&>AVlx7>vtP z$&=)9`HNAf!!+=TD0r$=GmUg(tv6ibiv86qZhl5pA23~Q}N9qe;Yf5+0aKq|u?AQ?kkUEwjFDcIzD$2DoqS)yJX)E$h{yR?*1 z{{(6+(tIp+ZG@$>FH_LR@nD|w`tB}`rzC(aR59-t;j||N@@1qS)DR@U{7bP$Iq^Uo zg@bc!j(E5+I7*Y-9zk1M8>{oCG{}Ak>1}-d&28YnWUguZ3}amJNlWCNncLNRFtr zYZ!KcOmSN6f&)>{1Bk~pXC5wF(9_|2hhd`M?(K#_$^!)-XKdX+Tyy<>6l~NRW(2Mo zmRlge6RjQ&)9p5;Odl3}6Q3yXbMDyV3n73LZzu)zONSs_JuC z*@y-scaMy`S$Nc5zrQ(yB`63iQi61dLK@c^s0%_iW5nI*@@Rn4#Ht-0`?CoLC)U$t z-?kh&I7tOOp%y>W;o=6xzcA|`|0~Q&t*{Dl8Tqbj{h zv(mr_WP<0@qo9C*fO4mwuj}6QSjn@1vden&Hpt%L+@~u7aNhtgaRB&(;xn18GJ5sX zYb=#hrPpJm}GN6r%fT(0l!+5GPCI5L0_kpIdW2o)Rp zu8@WbIUN^%+ALAp{+Yp^wyRRE!+E;aixRBgXulMZl!PlIEBlsL64dXrtZ{A`w>N6v zKMgyzx3>=i6w|sxpbSRAenrOb&N0jBu)JvN&r~qs)T|dAQ(F4w-%@MioQvzuL|}c^ zmoG28?k<_MYt1o@2*WPs*88K!GFRsJoH^j(;TgXxnC0uYy7!HZ#en0AFKmr`c?NlF zJIjiDS0MN0Im8qIJlBGGe-fRN^jwYESgf0=X@2+5d7rP)4d01c3#FvV5pagh+3A!! z>ejs<{{%B%Y(UA#>>4VcM~3J%JHK4$Gy+{N5}fMS-o>jdvy1kpgT@uqFC(114gfA9 zB=guvH%>MN`$k4a`hNEI1~0bzd~p-f{fz^zLje*G`XvrGH+QcSh>ErG-rioC9iK7y zk3$|SzEYrq#ZwryLDLkgIS}XL%ZNQcSeV%)1`)MMjI})QluzgRwJ*r!ckh`NYof$u zJNx0ZmEUny1U_J~#ciao!?-_6g7!I1xrQ7nl_{ViA-!u~&znJs%w_Vo`qq-Lh?R9( z{5dKIO+EFaVQ^58w4-B{%u^U9iKM3hxs0M>gP+LjP#Y7D>$kWD>M;+3haGPY<9;G!g9nj9 zfM5{4MGVIx>j4O^jgz>!X-W!8DTCi53_K~Iu5QGBsbvko$j)Rju1=#pCLJB!Vv`fX zLZgEesKw*+e`=1+dc97U2MZ`z;lW28cUa)?T#=jkNtt3_wClQ3zx@P&(mg!VAa%UGNp$&3bq4$v*MC7CN6R@n_14$- zXX9%J2QhR?nIHD1%cT_+!zwE)6|Nllz;jXJ279ko{Nq5WEOz*kz#2^QYc*J?STRhJ z%9*Z0IcGj6`3O`F7=cdX3G$hk(>et7AaUx1RgkkVw4$b_uDK_Hw^BI;->Vwnp!wTz zH5f-R2=WBQ!@~oET!0V+B24~O?{v8?3@;gV8!*%g6_MfzZq!PrU;tF2QH};;27VS} zfCGd=A9>!CB;s~FJ^HAZ>)?a3gB^JewTKhkNI?BVFShUp(B=pQyyHT4^8AG#p<`S~9}MP08vJz79hi&P7u6b6Se|KU)9 zP7HefIyjl-Oa%@iad#Xh0^)lqRez#-R)pV z)#S2`1@6UcgjJ0+Bj^(Tb@F;peFAHOv_u#dnJnzj?fE|w=pGa;bQ1tH@`B6u&jd7l z214_ykrXDm+sgwvfWYQo$ws5V8F3({6WO9=mmpnx%lvL{z~!GDA5${v)Uo<}dbqnH znRh>2l!3(ay8Qi`e%83+M+5P3Vi)>(y3*+{ydYGV^w~17K>Di$wNXr#Mk(j$sYR^! z!}UfSBBa^n8Oqh8^h;O7Men&us(F zg9stwcP|@jNSSSMdjluZ;(lQ5%pxTvMPeRe*Bt&#rWL!-n9b|v^yP>jI5Q$dE}~CI zSN9OzrZO73aCp*291chx4utmQ%TP1AFC#J%5+QRP1$nB)7zq#;t=naQM5)rW&!<;{ zpkk_OYBC`0wN(lM&rXbw|KPk2IyWw)Y;Qz{whZvlxyKaq7tdVW++Ko9pnIcHMhQTH z7PM^kdvje}g)+lVp|umwO~5(IF3*dc2eu=bHWEHhgfI;bzy4>E{#fbvaQjN70W=$2 z03o$^fDdItSU*LlrI8Q`cveL&fi$Z*h~~UA=QLr z;n5O+B{LtU=%cu&rVLdffj-3A#q($lqc)`=!?xRD1aXHf)sOn zy!YCluSbW;EI=^Fqs&cT`Es$>hSb#7vP;I1$AB=w`l|T1w`P=pa#b1%ml?FD)@ND( zC31XzE&*T|^UkoSZ}Il~lFoA2Fp ze%3$LVIw1>u!eL!O>+W9t$;5qhJUY#y{`^qK#}R#Q4kYH0ZQ33H%BBDN6x8Dl8ggb z3P_7=gLdy%?BoAb2-^)DY7D(~y%8q}bmd^$62j{8R~}c_!NH;TX~7x0@K24IoO+RR zpxH{Dm748b4H6q08zb9u8(X_(ma=wc-QiNFota9C6c)o+(3?zlCUS~2E6MX!UxUo) zGj>Yxm6ef!gMcK6<^nJSWK4KzY3W>1xny2<^a~cC|GGC$R=2i7XJ@tUAFftBPdA?> zCMD&}?45uP59tC0tx)lc0K_n(bf^K~`f{6>$362#-b`ho^%=tM;k^zuHzksq;FVn)&V}1crbF6`-dG1BkL8gKQ&<$lia+J^ylSL|o z!hQmv_YB@veD@Crr!L>cE;tqeioU;ls3?{miV9OG0Divt2AUHhV-j<)@?eHQ@6Z1z zUEgeHRePQo1C&}=_xf#^D4R@gboveSm=R=YT#xIsap7vOGxO0@PIM|NswUTe&jDY? z7Rqv{l9QE%|BxD#gww3|a~=x~t;W)ZWwd=0mr!1MEwWcro} zq9reaxZ5W#vE)po*Tyt8kC^XG-#^9M8tHgrz`v zvwgfiwk5*@JOqF;?Wn$PtU6PeNJ*O!3u~nP_pVJ>cdrnPU{lt z#i~fP=3{Spj50GbJ#WuntE#GESgrjtePq&Zuq7hqw*LeOf$^{0cc70-g0=%oZ;&-~ zBOROOhPp>{uA>hMe-_@xIox{Cx2`D2t_1`4CMq~Vo9q_>VOV!OmH06T5Q+H?Q$mIF z#r_;S&^5I}KHyCw$Pl5MOPCmLmcsc@%Z`q)7+-l*Vm!A zxV_f3W&$-Wou}S4G8^B(_xc7NkiXTLO`kI9;WC!aO(BW_q$R!A+r zIUY~eiJE&i;v496#JsLMP?gfDGWQHcDH!p94*(j3X~YC*powhAXSSy&Aa;S}5{ill zpR!Ix6~d@h)m5cY0*GwB^VW}7gMvPnWk$ZT0OI%9Kg+%Y%DPWg8&L%G^V9=Q3qbV8 z)_HVvaLo@%|Ly92xt0U3i5KCo+FM?%hb=T9Lv4l3g2Y_-X& zEF=_^B=GYPHY_bJN`ng0<^tx;`OdFiQ+;E_=@9)+KSAAvvgr^q9>=cDBcNvG0c8<~ z#7E-Nnfu(kUJQZTA(;oN9@Z&`&-zkMz+=Pv>(d%Y2Q@B_RvdDtWg*~>cuo88(Gnm#sE``(3+w2IYWJ10fsqs+u7_m?{T2?d zr^oi32k@@G!NEZZtFKTo(5n>EGcvRpqLXvsU_Cc99tE5H13+0v7Z>r*4DSIyL<%rqzJ0l6xAUrJ!V+!hJb+cHV7fbRlBNO1aBis_b#kF1`@A4b!8ag$yt^uW;G zaZM~~<9lhX9q3-+*c6h$U0pjojK!u9S_LgDA}&r!LPDY<>a#|L9?=UH0||f<$~C4k z8khxb)WQ{S8ugkS;l#r*fU0IMDmMGO;{U|?ZB40?liRotwRX^q5SiWLXCI)l6CJ)j zb#@B`)Cs7CN?DS!vS{DszZn{SzD(F_ptA@!&Co&#ec1!f|5&v(EBY zPl?OVZ|o?5$V(L}W+)fP;sazN0u&e~KmbRXD3E(`ez-&;l4`veivB{mRD%ij+~MKj zCcg*oOn(#$P>mB4$`Gr6MY09IVe{%&l49*Z@Gn zX7xMYcAV2~S3RA_3Et560$-!jAV#%7b`2;F5LoUT^0~L<|Iqw_N!kTaWet$tA|JE8 zrdSegEYMotXjY6~dNhJQJ8U>(!Y9*-326@3tG_eX+X+WLlA4 z^IPDHhOMrCfC)MHK*-bG$`d62on`0k?i=|`0U}PDX^G*)mpJ(NX3E@*h8=DFk6Nt` z8vkMY2>?@byw3hk{Bv#kc=LACnRg(TY~;3L_rhDR-5Vdq{K{YF;o+doqvtC}p(#(+26kLY0`vL z4NEVk4Zf6v7VxPT#7E}lbW7oO9@zV}zdnrun__)!O%zZkyHjnAl{?)=`-I)8(zNtO zK;ujSnM7^{)sP6>{}hE%jq)Mj+nsVePpL4rI5ZqsxdLr75#Z#vS&$`5wg+$I?oS3O z%2vE1CB$M$d5u-+MpI5GY0Do?Bp1$GG#kjJ7(VMKK3T|y}nQlTFJ}|o&7sv&@ zzY2&eX7Ig(#DjjO%h>@Kxwter?UgD$9XZPN*0VkhS_Y@!w#28~_z?IDxSb2akZjG%w_Rjtca4C~#Z6FZaJ~Ihhz6 zJA(cJ3K1gBc_ZC0MGUitQJh z;L0_kfhk%uiRfZt6A7vhcsegap#LfOHyRu^7Gr|8|AagrZaFLTnu9<G4t)h3NSnXm<57i3^2>>-7`wTZ3D%OLr#umXlNLmTF9x@T3K)+dt~8$ zwGV)T6?igwoqlA1d3S+J!T~o3PL@_4J`Ma581@2<#T;Khk=z#gzejebApoL)-hl%# z9W2wXMQh>5GY2_AIE132GW64*S zDk>^FE1kl?i2ms7yLNOGXXyVhwO>v4U+)9sOI26Pr54WVO2e$Yi|XXuaPfCN03d(^ z$CV8%Zo;>w(kWE4pwwtMIq|`dBM#~Wh_-6qJ9i8UAyUv=`(VC$191#8utdTQDhC0l z$HBu30bXpbQkKw!CC67G-xl#XP|*JwfJPC3W-kV+7B=*h$`RopDqlU_8bO2r8vjk< z&KICXh!J68db+J8@MW0bM40uOx@ySS%`u#uosIDuT@iKvdyuN?>Qv+^;ghXeOi2(8 z>p7PEp2w6xlY+GQS#vO5PLeG9QZb!pO}XK}2ekz|>&pKkc&T2#L@F*W2EA(#v@oaj zzGr1%UZ=-A^Wh9QkI5{C%lRGB-ufw^&GIKR{r3#`bqk(1o$c;qF@SV#Am6)KF}^Eh z#R7Y@8a9+vE>VA-E%qk2!4kcPusP)J)F8N)0^L6%if|)OI+a>g$6P z{(ayzzktez1OO`>Gu-hI^wtw>BSfx?&d6kq>7s*U?mJ(%F!=0wgJX4nW^b7DUlP}b^wEx{ z-@VCao|}PZeyga(2V*vnkkC+~dFz>q7~uVg=l!Ze;}iBe?K^I!nM)OC_$Qu(HSLXn;9~6}ccb)%sh3}DE;3mycoCDugcIz&w&Ahb;6*9yJ58|nHkcf-)tVP8 z=e(EV_%e?I0bqs*nJjrfG66#X&>%IUs3CxBB7oiw?8J=!Nk{tR-00qSJwWG(po)UY z3`B(a3k??+0c7>?(EgZF`03t-!*&+Rq}w1NA_AcX^FrV$zzmG999GW(8rt%h!?Yb3 zXM!;X9GEJ>xJ!e>N-SXMtAIg<1pOeh7eEhRxAI&kqN^vMP3%6$6W0AmUv#fe}!2($dl*fJnf`NP}hltQchwZO^q_40GRWBE562Wjv3XFHpf~%M;%x;y+nZ^8{4H7ic3`XK# zXS!l9NYDZxkO&>Po6ve`-;ZcQ?98fCpHxk=|3&<-t51fPa(5IOfnyiTj>Zh#p9l)t zAqY*RgDt02v+IzFsxky>e7T|euT7PHQi;OQ^?8l2d>@d}=_~AWZ2IWmA*aSl=gda# zm@@sPh#Z_wdrLowXxvSLVN2itiJAP^DaVlWKM2eyn*0`8Pi;d|4__Ro*j?;i_;D9M zG|^*1nlRKDB{!+wviVwY+CVW#EQ2e$P#L$ZJv}5PdWD9s*>Q9cCxhTN*^}>&G_31= ze0Q^1_TJs;jj+_caBq%W)aVAe3cVLPY!`>0^+{SlY@^P+W;SqwhlzkRh9$yYJ4TTp zCS(j3=h5w1h!Td#^~}roDlcl+nw_9S;!v?+sT~Q70!nFLzh}>$V(aD&oJ{zsEhZ&T z|GhsqD8SQ!ZQpDL9ZN8A=_&MdY9`eT8KX2T#`EqISG#gDfY64i$GdWt?A$OF(@5sv zA;6xNt=#D!(jWym^cu^8g)Sd+C_oGoCDKxN&KL%FEdu}|7jf~*aR5X5c7!1h`J_$ z^&}j1f^=17OA~RN)-wJGRdZWB-rizKlzbr`tXz!%oCqp=KGekNym)74UOdgls@R-o zJ|SP<*-_cQMXkshN1B=9&Z-O&8zzEn5!3`ggJymar+bxj6&E_|`4w=K-P9l0{nQ_8q9#7l3PfwI5dG zECLVyFXRK`n-;gza3N%0{JTzB+D^>WX7nh=4Ycwk z`ou7iPErF`)rfl9ywSYC>IN@6MmlQQ{>8eOYn`4ayxQN{q{TW(fZ1FFzq(w zOlW$$O#1jD7#iglHcv&;7v&H6jY0>Gzq#R`qCZhuJ&|hQ#n30kHy!W~u{eY*WaC3q z9KN#5i05n~-bTy5*7*8kh@Eg^0%^&Hf5ScktuX6{)0rJZo%desi_Ky21qS1fs*)jI zQX4n~4h%mNVs+$L?QJ*?!}5(~C>&64kUDAAD&NyEDSm0au$Fioe0$+mWN66E^%kQk zvmpCRe-&wa9Y>*m{Ot~6+mi>fo`*kcdU}D?Zw>}oMJhffLNn@!3ey~CX4}Z?=j~YY z$J>(OZ?5fi&M8~#xxdBbY2;`8nWN#>FI0x)FZG`IV+P(HFln^vKD~r;@7PVT5i(={ z%!&aDO}-8QN}pUo`OXAd3!tUZjv!0w%#<*Px2U%YYeklh0TdHzTgwol4x6mtnp zu|XWWlbQ$-4CA`x6e)9@N3D)uPYz=~6r}E~Oa`R^(S*DXk9O=pT|MnOMudF94jXy8 zeiC3^jI*O?tjA%=jz;J7a88x{jJ;`wW@$jiZ?dUgqCwdfFQHf)ef?Z4JH`(|;aIR} zWl6!MN$hZe;9moT1{&;?IKRd~-%lon`kdQ8coJn$>wr1qd)LjMYA%YnJso&*nrX4$ z_j0uMcLb@d?&_LOX1K{$Sl#tAO7V51nH$f9(O$AR($npO=>#F5$iQ-qZg$x=243@o z^;lt$?@9j)n81R~jDU2(aB_0`%4tIYt?C<*HcLsZ|{0}b+RJb|@R>--#r`5fuG6<^Fe}YQU$||YIbah8T zZ-0m=TxCA*p`7qFZ-ZUzS%j?pKedQ7tlSlrxfhN=yh6HwfUl{u z13P7xhfC<#adqdYU|0r=joFXnZs6Q#Db9khJAuVHQlXE83aK1$kWXN)!EL|T1)JOl zBA|#-C-8h?>VLF!L)3NnQRdtwKucOzoc`C<7s zzK9qVp4*A*T-=N@@xm414=OAypKK0N%Bw;J6AV{klhrzF=dS}fb7H*5RPz$u)9g=G zf8fYc(l0=QYA`YELZCbA%i_3~L#xTf;)mid&bSOK2A^}p{CM0fpfMS%=4Du9h(d_}u|ySHs7 z$)ievDmxRJ?J9UYG8s%+gw@6RYQw(nDy(kdT+aY56h2??8f7^AMa_K3)IzuKPL{jZ zBby$%-n=iSQ}8lC41_2Wet;NahcxDXZ;tO7L|?o>%hmWhN4(y%yNN&T_d)~h0ChZ# zdptj8d^-G_F# z!b;SYSfudrdL=zFZ-Rs2E=gY|xF{Tu3A_X2SxECvTV%O3!>@((UGW2kFMWqj)d}eqX=%Y&rYT~6QtFNr-a>? z6$kz};y1*difcQFW~Ng>T!+~r?(!(IWjO1c+Jjcv%#+so{}`yygpC9@4T}u1l!e~B zkVa0_pwUVQr>hiGE*FcBeEDk+uAM-l6xo#+YM1JgQs=>y{LP%7ojmKi%G?VZS>hes zo@`mvku( z@B0k8!5+7*hGl?z6-eX!v-S+2M)*J(h$~96qN$4x~cw9%JtUM)t1{J8YdJQ zcLb?@4GT@#(?qTr|c!dfDWm>iLfH*kf_AM5*ZgZudlgCwCniJe{XsJFcFEQfn4A=-?KV z7bmaT6O!sK%%{KevXNqS;p9JaHtjlyey7~oVXpxR*~1a}Q?&nyYd1zegq)PHm zUI6po{lKG*3ey+uq$d& z^yXv5PZJC=>}pJ+*Ay~XWw9ncdPdEz2&2?7h?7X|B2+qHZ$%qZ^=|YQ^h)qd?F9}| zlvt3xo3Y7}yN2lZpe&5jKoKxR)c^WXkZ~_*S0>W<9iZyaRRz$$Rh$LG%_^5|MGUY7 z3O1eCZx$DAKX{W2Co^oIHhj}=aee)Fl#N->?YZ1U`H^zl$1AwsO>J&Nb$o)EZ8c@i znuMrww4UTfHw6PqXy+BVRSRx9?B@QLv|l~Kf|JH$YJ($X1-;AB28#ytFl>h424na7 zBrFI-uGgQJDl9uOKUdQa6u7^8`_L%n>4uLOhZXRPUi4Z~d1eRh{hyPFI8}I+0hjGf z19sAQ{psW)<$;dd;kU&BSN;sSdxDLX; zMJ_`O>SV%((3zQ_r*qb_TaABp(hoqksHZ*=phu0` zXkx2D<#8Z~C?aM7=zM`_yt5g54~aa4=iPuAYjpb|=Ql=PpS(D{NF04I zNCuvMC;(<7u*>r*%g57`l8xi5iw4(j*S3U-FvKQ{U zzn8Uv5-?sH!bsPR8=d-8R^iPgG45~au0XOui*!v5)vg=%BQ=8$@!%Hs^kBb+n}P|k zbxFg3z1_50KiYSqP9}7CGYwy*t<%yM>JcT}S_^rDBXNdERsMQ?4tiP*bys|WCT3i^ zCe8a0p3}emi@=j>MT0gI1xeJ2gu7h;?pK<(Xj;0FbHGUWRY(SE+5Eun#;na^LOJt~Q0OM~07lgb&A}uv95zR6|sY*b{R zS7W8ml&6v_E92o9)ztO5{jk328nM(4y&d;*#?asBcfQQduRIj@{aK@$zi1Ha$)Z3o z^b{%l6F9=zF(KstxcEO-9VZ$FPh|NM%?l6mHcg{#vW-#1UWle2szf$Fg(?o6p(>QA zn-ycA-d%hQLWNu&BjQ?f5`MBH&YSvlO3p8NlT+gV^Uo)syc9xtO6o&xjuvI$7tngQ zwu)tQiezn$KfVE)4h@nW?S(Q`dj@@f$1(m>dr}4_>h~mTRJE9rQjB-!E&O81;Ah6S zMK9K!EBQ?Qjc!hxM?}5*TQY@N6WW8+uzW#w%93B? z>#u@W)k|@|ViX7b#T$rth5zGjFuSP%sW?u_;^BE8ZcnkIrgSP+P?a?as~)-Ql3&&A zQJYrf+TtTge)`==JV9}r2Q>9~8m^_~H1x;LkN46rUHZEC&I>HH0;a1RztT zz5zOAiEUJ`bkM>ff_sDZ!zcWAN9)ny=Di3hGo^F2ZYRAYT0L%q(}v_H@Y@7HM)d;0 z)wP|bIr|UXAZ$_qocKI2?F_pFWL|`kAP)?6u*~GDS!%HdYWn7oxm-odAu8Q&Ll|QW zJZoU3iS;-Obl)LlvCryE$E63w*J>fJrSPo2rRd!W|1B<`=-=A$b& zT^I&gC}n2L)?1eil@)pON0c-M8zdn1AG8B?j&`xP_Z{W;Ui|GUV4I|%zlJv%QSeM1 zVr=>hOCG>dpW(MM444YP~hNJ6=ZPyxGKjaXv5{gm zo8P2ZqQlm2x#%k#5C|1@lFKCT81qb1OgEoVO@!Q|Y_*F!b~L|GsdlJ2f*2xJOb&|u zi#=D|rCbn3vG+ zMFzJ9-ZhAk2ZswIQBCxABsP5;{`a+*!&`m{H@g0d0y>3~{a!5Z(jW6-)8bp(JWoIo zMjbM=IYS5o34vW0K%ueZf{qrTj1arosRxtsR> z4cYtfMXCGwwO6&X5K8+=3VQ^9b{lpm~$9hXZli zfP>wLUnH8lpu7DqiFV1!PxF%R`LSNqsL-j@izdMzXd30S3QLuTLWBCoRz&=q$V!)* zA0(Q5t0#I4v`vnj{tV}y*(MIA|M9Q3^n z%G6o3@}tLWNf{C_rc}npSJfLyB&HSui|`5VGH_-((ARz&<-YfA4AGD(^Y(v@y8jI< zzuz?js~`M2TYPG&HMHa_pBn?%N z1jw<$f_5KF(ZP1WfkOe?tOJ+D>Lj~`W3Q5REEWe|p zU~f?|`FR|K&(qhzP5bij?#l^Oq7~t);*Useep@41Jt#<+_|1`<5G?1p3`_O49p zx6Pug3<@jf;a3_3^n4yst**3{1v^o}#Q-xb-lRZUk}^!KogB(dhG?hgm7=F#|%rU{Vb}(lqeq0Qx_{wzHMLPOu>PDduaw3 zL#{#`35_Cx6bN`VUHek*h0FVyu|J=VGoNd5O@*IX7TzvV@`@20P`Fq}GbU){wsmO_ zQZ`=04fp<4FJO|ILSSN}pRyR6#|o2~2XSXui4g-ZFb z(?h?ZO1se8ezVfcHBDRO?eD>B>Xj%wilHVkp|=k-5>Jy8RSHe;*H5>LYq=vgIJ{$+ zp%=W?Os8^t3Nz%Ib_kV`ch2vZbE;y~>aX&IUXm9p{iFtLJzq%3&MkSAxM|r8#D!z$b?i@%s>*m0 zw4+YTvO=)^zvq#ok0bOa%urOud9$qmxT@hBCq&(-1K>solYcU)_gz>PG$dv9QDKn| zDg6Wgt0ZwN^cYdZGMxZDmbjpN>gbTt=!~0@-{`_U{_>u!vSn8pm4ORsyt2kf_XP?3 zR1!PVERC8e%C@fb1M9TeFg7Zcbn`KGuAjB#dH@?d+amH4)_CMybUs3gE{`H}JO>GU z){#<=#pV0b%M`5*oDmPiwjcxun~m8IhuuMaIv*0eVBJ$ zCfCBpcLz)#&GiH81{qO*a3T^etl-`Ra#9{}Aq@9oW%um>Dxqe7od)mbD-K*VFbl z&i1)~f>d4w?J!DJmGv)#hWna4bdRj3$wF+Kse(16vgM-07E9#CNQKOz^P%A{e1Zb8nNYj=MX zcp8a~8ak*#F@>`wb*iTsKOnAi5Z#MK$Q2fs9VDOmi?gtJhAy8+)ZF*z^a`eD=Hd%> zzI5w5jd$I?>h+|cy=TWD@*jsK7N?VcPO7aVA|Vz^&~G=YgIk1;-?PG3PNANk=1~ig zvvlh_$M;K*AO5`p8dBs)ZJ4=pNgxj5>FYz7j!_aLQ7*9`wG0Ga>cOK9S@(2W4P&PJ z00clWh3drJNX@BK{mT%b!ufk}km)3;(UM!eWNwO23}3!Ay1+rgDXJ>Vu&#j} z!c4U|@e}psF>xm@sm${CGCtzO0R%{<%3FbpZno(rJX(p@BYsRczi4i{^d!a|B99&5 zp?8BXU^ZUr(3d#^_eSLY1K)_$B?yqG)xxW%lm{2M!1*Q?SaEI^QgWiwf8=vns>L7x z(6l#n$6?DIc8} zb3aoeL+&RsT@dSxwo<98qf@MGY)KP^2%B-(=}n7Kf{6~^ZdsN#np>)tloObZCQ}b) zZR6s6GyO!Ke6?m`nbejM_;Z9vyEp=&nsSebN$AA{S-L$n`o7RU#W6z3*7ag2J}h4^ ze27TS`GzWOM^8@b_<}r}e=^s)eM;h3K(UsLc>ApQiA-{R+5v|%kBhocL*l+gCEjoa zOW#=H@@}tgqOh;^F zB+nbm{BLB9E3ofG;KJw(t96*G|h6S=hpXmH~2#(&)fZ;-e;3LhY`& zt2tcqRh(F5GSR`?^3OM{KS^-Z-iSDvNPWj*bF_cL{AghqN?GI{d-r}(MpH7Wj@-(z zhA5CQahk@CK*vcUWJ(B0xG2_0H|sgdO~D1yG=&ZI4~cTf@Nq`=CKi~wMz)%ZIJv2% zxKG~H3&*k!)08X|fH{^?y%miiwfj4k9W{lFaj`W)R~xPSXKs`PLbMSS zI#02tm&cSZXujYYn;Vw>(d78PlrTBN^~6Zp*Y$_>dpegWMKsRt`4JhO3-!!5@8~3H ztIwZ-5zwlTC(>lj6_7mSB{MTIZsVibnEq%+*J^!Ba~O_I&}2J^-<(o*TQmA?K0V=~ zTsa!;A0M0<8t7r~Q(%#EG^svHsDj@M^`AU|HqT@xu_(wMFrgV_7RyxM~N zVo(On@g_jgWbQk(ve$*y^%+I{M`xtX7c+rHpXcTa`8#7(jdSVaDMI(lMHhF_cU0Ho z>z1?@G|mpO)*neBl@6DLAzJQBiUYvu9%A+}7((#dQEy9;GtIXYAfv~F7@ZK8b#yU1 z3p9pnW}>6-toN3cyvT2AB8ZB%>2^8`Zt`Q{@%@bg-0rg7tT?&UPkTXmC$r8p&5WoR7~&%Gh1HT1oJQ}Jx%ZT*xausAU!r7znP9)!pUI(7 zY;~2GOOfrP9L?9+e!gVIO5@@e`=lH>*dI=Yzt4z1+#*kE&}&AfVFc+u8Fu5e&VR&A zE_2E!wz!l~FC=lzH0zG>L8oLQlX3DTp=Ij$an(_@vN-x14gpda<{SM9R}TuNx$!XO zR%!dgnVOz`@@9;L)GTqZ;DBexOhsDgwlqiKViRNii3}+Y=gD}*-x*^8`ph?MFu9Q( zrTk5_({&)JY|IJ5X9AuJiOB94vPM4}12O*$`gdW?QXAU+F$)^_dGBBD%`9&sUffAV z=uWMC_dSoqk>;&Mdu^c+xTlQ!qJtY<6pM3 z3YbCdU3N+K6>WsOw@Ef$YeGirRUlIRCx+qtUa^b=mpc`W*g-4*bmY8 zQHtOh3+a*xA8ntFSiMlUhO2a1U=&j36=Rc_5(+JvFN8q!Wl7b?Swt_+Nma8l!kKT& z6}C|z$03eSr%Ja$venPRdIL*-Nk_yzey~g;d%|4*9(;CT@VlUo>hXxBw%$a-D-B6S z9i}8se;MX9RWIc``ds4Ky2uWVdED2aqqTMpLC0mo1PSLxqrdwRX%>;WfT8lohT3W+4(tFV7r>LpjdY3ky!%m63~d*T zb4zuyENJbkX+9Q<{Mi{!#r)D?a||7wG?KrMXvPpna4*Lw@{ba}^54MRBrYM0*V}Wv z7ZvS(H6@wye88*)I2dvl|M^(=n((Ec7jnOa%i6gNg`-|u`F=!H#pj$&RE4RZU=sh< zb}x5D%*(>RBO?nM6yh%>{^huPb_h3UEp1v&T~9~JY}ZcYZ0+^><0G01yS2EJTdg*V zyjDAt0NppMs#sc;}Zi)CaYnB9c8`c4^F>4OC;kcE(fx&toJteH8T|g z29W#vHuIvjITs~3`9!oj5~l9bg17Mz70Sogc)e-vQ#yjL%HCMnnAHS)M^|yi_=5c< zHc4Ayc+jt2?jIr;0iMwTYr&-6D|Q+6>AlLa?j!mAsRG7<#7m0;d6m&Ttme2TVH8Sh zcV947K#cgenONlh;(D|V@n|@FRlU_EfgmSG3#E6f+vQvmu;E^9MLGF(rmz$?A)mJN zW*)9$+LG()nNLyIq>Oh#f=1Uz?}_Z&N7N6q3Bm7+$>wa|lDv!+;*foL*4(vU;f0Iz zXCRFF_DcgY%8;ij4scA+WcF0dDP((oyRH6qdJCGDcqCFB<-rXN@D`b}=+=2n!~fD) zo0CaA%tCAOqI0}`7L-ZV8qWL~eZJ@%R5^K*e6sv$uyV#B7Sqt#KDa=*cXks^9ORr; zHK6ka8apXPC%=Cf`aSnw?v<7k*RNfZCueH7tjA08%Dh?%Lp%8@89c>#la#6L8e~wK zRj&Hdss53!7DI$YnoA{gMhiz*L%z0B+o z8h(nCzOFmT(epEIc5ep~PaS=Kl{{Zyl6n`^68^-7VcAU4nEY-Q6J4N=tV)f`oJl z(k)1Lx3rSd-3@y^@AtQ}yEFTjgUkTWeP40Trw*BEcuhE;i}Wk@2ljJCUmxNrQNFXEG+zs{f1jKCSCTYx9?gWG22}}c6~k;ZgS1Q$Tp#i-kA*%4zgcW ziKx*eO&SS4AdK$XbgIeE;rs4t%8U*_U~{(j$;<<%Qo@N=Dc6FiOv%qIYyE{+Am~nZ zbJF(ATPRx9YKiY*a}`-TZz@Rj(VLEG#5&+`5=PrAUgPgER&u4?mexWT&EPAID7KhR zWVWLYvj#efv1RKtVdz^7G0kgy!aix4$`a`H<%f< zwx*!8)nAl=Ni7uN*c5VU!r~)`TFp7>b{YME1Y@T!B+|3Bn5?YCq@U@*GSI2SWzF?Y zu_;l?elkWTQury)%V}|=JzC@@tZwo)#_QBLw1b^aubw>$$i&o3-d?|pv-=dI99fsp zRj+()R4sq$HHg6~c6d%s5C2iRL9plZ96pl#ELS}scq5WBv@q$~{Vki^bC6e?Z`Lkm zl$u<#Q7TpRmIX>?sJAYM>s`3f{Em-9J%tw%I~mh4j#9($M;t|=B1o>R@ZTu|8Qao= zDtKynvsEXYFV;f!>0FLWTKJpy0Fo>XEz8qw)a^9wWMI23#aGT>>yWrpGW zU>jjWZ7k;Gn^6;Jc1R9C@=mQvPnyXCh=~HQTV6HJe)=f!bS|Hr84XU%FB^lqBcKs2WB9?qkr_wz8q539{DO2)7hWDgIdY?WgOYEFM=|?`7@W0RfIe|f0M)67y^~#U z_L=Y-^fxxCAq9SAb$)+=uvz+%f-9XU=8WmN*Or|QlPISOnjCiQNae_&@`**4!dwh& z{@@TzEuhDfzw=YEOyy23@@onHJ6{jdZ=ZiEeO^PR0w)I@fk@axs(+BxffQxYg41<6 zsqeg0HQo=SEPj5TC}oqGa`tT!64_3 zvF7%z5Chq$d(P!X+N)P9qxd8pue+S_{=E*$_7B@!^PSnhmL}VYhat;jjO==BNV}+I zJAj6PQW}YNw3ERK<8!h6iuz8cP%*V&$C=1yc?FiR15;`X2WE~l-%{Yx5I=56upm$M zAkXgwYk1i^27#2#@SKw@0bEVzC_5?xHPoY}3|6@q?HPUKjRna5alHO)q6tq(<$(T) zu`dIa9d9>j2s$YWJia_y5gT3HY+QF}p-mSx_Gp>W>B>nu>GZ;Z^QIWNwy~_ANlxq1 zXK5_o!Rl1~QaZJ{({sqUP(LAbs7?qrO>*=BR@)vPUx#Xc`?7yK<(c>QZ- z_+Mk(DW&RgEgf{I5FrFqZ_oAKLa2;6+&K)q_2GK47#kb?)7z?OIqP2>fWfJ{(S3V!PhsAjB+Gt;#u~+Yu}=>5lNm+sXt+8 zxf)XgND*iisRa>7@9Iue&Cgr_$c$yk*YUVVS~TghR|M1cf+lMl7Bs>}je((bDjOVc zbDuxVy3XkgYp2%0cF&ssVP5ujzCG-60h}FqNzw&QZyC^RM*aNL>uJ7y-PY9v;c!K& z=Ql;PA#_ADC-UCs9IefJ(bW||Fi86-NF13#52fGuF4$%H?DYAWw&|G8#h2W`v`Hu; z15HYK_IODrY3(%ROTq_7)hjrn1}2oUak_<$p7e*w7{cRxI4*U%?|a6{lQK5%8oePu++Q0}nm2!{aJG*hS(up9{_iKNGpGK?y+@ z8vS5IDB4|{UMJC(gWGBgo(vJK8>zwl?1Kr||9*sQEOLuCffkn7A8h6+0?_RGHC4tb zukMOit{ES9@#V_RPg4@yGtq$bLgX?+U9;(2KGN7fOftO-+J%6NpMQlSC)sR%4|o0q z`lmRvuETvYe@_UC8Z%`_H33Au%n;q*nr(cVK(wl6EMrr>p}5kKgU17;s{Ab5npsek zazA=DaP0$QC|W!vI_sX`1!$H^_#*TsiKFZcbdMls_f*E(+=~u}P+(}=MarMjzr0K* zA-G8j-OK4mv=9QsQ+DHAah63uFB&L~EsnNm8AS+0wM&qJ3|Rsor5As&X+lirkKXg7 z5e8|fEU?oX=J(D2;^h}qj4vg4@9K~z$&37Pf-#hhm#*BMwP)Z{Ya~(k3nFrkkEDhl zV{?8k&2akquhfvjJxXnZ^Rq)5t;M{iHq7(rL&n-=!suGfeeQX_oj2)o6-QZFpLoa4 zN!sKCLs)U^8VcNCm>Qjr8gPVP9!)*sU~LYHF=N;xWwsPii(ms>%CHxaRIzu>C8-4K zp^V23M~}L)@pN5<$h&HYQ6C7D~{x-jP^pL|)jCU5yDkogK~R z&qQi284|eH5wqhLWpFv^sZ`^X`>I$Al1G*9xk$wwsA2(vg655Dx~<+p7{ z9-ukI^vAdi44DwwaZvWH_ZA`&{5TYKsE{k{#A8*E>l6}EOJ^wHhsx1EkK7kaHrXHc z@77}}&6L)jbifvO_6PwKxs5kRb)UK-VkUs-)_}e*I~oQQ0lh5t!M_C`YHJE1Ps?dD z7DrgyH^9*Hk(B_I=#WAOM`<_%2$Gzh?NK_1_Kj%}%xdyti*5^JIGrngV<|3@B`qHd z@?09cb7Gv<%Hm>st&C|AbVH813312*kf)$y^-D>KPP;_`dM`4@OYG9-pDB@?*Ib{f*!Crdv6O z&{^lH;zq!uA-qX8mm!R|pQTSq76|FJ!Nw`RyJbIm3AUK^ApPh`5Dj&^ z6X7i4bNbuM*z_;)1H{a3q`3 zh;!x)fLi0CO^7sXfvAE>dUZ`^I)aiEkwc4MwaC!A0E%}jzuINt~S9&k&sOxCR5r3B9~ew^x9#OmWjV> zCCGCkPGU@A#eyOAE$LKy{|^7p6UMj%!j>-_<4F(ST~&BUmU{YKQPv2w>xW;OOG)7! z;mKlkD9RBiWhhF6{!mc9>Ua=D(y&7|u8M{m-0y1?K2Xb!aAnOjrwpI)E;+8fZ?f%t^zEnr<`rToa%?K@i2bTxPt_fK;ZR;$;l+NxYs zUvpHSg@p{zt1^`1&RORDmkauV5-|>2VOl%5=s7Bj_`!2udlQhNjn9qGyuYcDrW|T! zt#FDjRGjqc6Yw&bZ>WssbT-|uj+ofd7U&NL`NDd zxxF*o({IPsTS!D>yiHndHdd&+FtVJR&1CbIG%}<8jSpK#)A&baA}Tn!haD)W+sO@* zzr?QA>KLo7M=_sYoiEc^3XuA~FK3X+itS#v|Ht-)Wp);p0a#6S0=BVGe&j1*Z?02V5LV>3ZP#j?twTu21!3p*az4!Sb&T`v?IXE_fvetCG)K~lV72%kBM~I)MmWn zAQ!(Wp!NS+JWl?qb;BSs#Un^E(M44_aZjJIuyQ-=T=5=?P?Yzs4DYu)8;SII{L{9J zJcK9w^NYGQmAqa`9oXM~?>eJD5$L|N(+#%KY2<(`Gf6!4!|O38U4@oIKMvHBI_NLx-e zWQ(h9m(jQ~iUIg8=t|juGat?qMY1$SU2Y~-tlclv*meK7lb*TF`8<>api(h#(e+5{ zl_~p(xvJheJlPR_N$y|8g^us4A;XudjEJ zqOw_dsvRxCyUk+&`VI!ALlw*_C>=^y-k3Z{GCihgaE>G8pdJ!!g}ZAxeC~w0=r%_{ z3Ormh#H}e^xyqdSUh6cLE|aP)SY8sTKlKAUs>c+~;HAfw0V?H`zGf-xHOW18q=z8Q6Sp1aQys3-4ZEj%@m`(WiAiO#4`5Hjq#ElxtRY5K-$a-_v z@*=;|8Ze0v$Z1}HQ)|A`7nko<2)K6hVttUICK;q9z!o(W;f`%wp^Xdg+E1i1jEOJ24-RjYZmL#zsRgCT`YcJD?~@O_$|r?`<#ZiPP)yLv^Wr(QU&=>eT!;_W1C}X|JaG9Md^tAPWH?b={Jo zfKp(IElJBw>^fkSIZ4j!V%)~w5BLQGy_;k2M-~G>=Q4=&yjILeFZVM!LJnu%?s_i6j-HM{atIt$lEaIl=tq$T9ii3E z3fj~O`GP+-i^l46=6s7Vv;vXD*v}G?2-R_*;+E=Un z=+7S)Cnz{b*_zZLEOO5eFF^p!hAw9Q<2f)l5VVvb#7wSD;L;sV3g62*!hQA6>6OR3 zrsiiVDI?%E;ezh+fqKw{PM3C5qu@WQ2nidBHZ*s z$?z>RUC%dpgGJxp|J%2tlW~YMZ}1dwS)+Z|;?fI)P@vzmao0M%Ak+#s?3IzC+jK90 zUy&M;w`#KP@#?GS0TYCKoY4f`)zB6tu|^^pV30Om?!J7jp=4Y|nm*RU_ZTBnzVZs7 z&|I!o!b5&1QuC)b4ru3RMGs0m3Tl--*~0QwB4r_9B?b{qa;M9mH%-@R(qtU+aZDvV zjy6>}omVWD3v&zulr{q|iihndYjM-r{WrDN=@_CE<6h>a9O50#c;S*cL}s@y7Tx3? z{hD0%-tYKt8;!;bqgdH7SXUY}0Eq?>8Oby{hVu8It_%`XVpL718X!*ZB~9o}smxxW zPD-^kHd6q5;o&Kac7{UgOW=C+=#tg!r%!}zB2_176hF4o+cnX| zN^yAmk${G0CT2A`@EScU{bY?HXbSI3eI2lx1Vjgoh#aAjk1on4=N7t{IuP2{`VR4+=$ zLm>ueQWW401LqJ_ZU9yX=caCs%F$ZlCP#g9UtWwDn%qI+X}VL5?!2;`%Bz^s%b=Y> z459>Y#4j|-Vt<-M0W^PRlJP(VO3q)p1<9iG%)1u+5Hn) z;EDS{btW3r?n@-F38}C z7tP>-{SaA1`N~$WC{r&K_yqOdkegH>OmK5d+<%R$);9oL6t_L@S5m;ZSuWbQrN=a( z`3GMXxxJFrQV9^Zqj5TJw$xh9%y0FEnnKkVC%lOJdsFJLCV0UGh@~SxW%)CUt3KK~ zOQY!;`U2Wwpvw(yf?ymQ+n;hLR7=R9M9Qedf9m)1+IYbz{vkIqg^{|3qydBjOL`E$ z#~Q}A&6~-xRaYhW$gu10Z7zBUx>%OJDes!4ilRh@yEGIX8 zN?W+J+(Y~T_0-bSt~?~*=D#B9#333JpXA#Ky&sq0(=nE)X-4ByKWM|=|5sT{Q(W= z^+uA~--nqIHC;ca&!#v#vDVk*+4#=lKKiAwR&=;Wuy^~hMw4*EtTS<9l;*;=u(fBI-%l=_4PsaS}#=Moe!3qvt^6fV1K%q1~B_H@h51_%dNf zK$3WXEFlGuv47ti?6^LAqo@nh`4=7_g6(%uNtHTih$*d6GN(P@MS^{#9axS#65;R|@{z_3~NsM9)U zK49&m9WESyRk<>$6V zaUe6-Oy^Lp9QZ9Sl$kmc`XH?SN0qg~{Qe!eArtf|Wgr$%DLQVQj^~=}nB30bWM_|+0 z;*gF4YW(6}$#xzC_`Yc6_e*vgyL_Wi zz~RUF+=Tg_&^p!R7O|92N7o^c+(+@G*B!xMh-e3E@2dH~n#z0%pS)XjI_)7b@D|Uv zhQ(ZA$eT7j-;9Z{0(W<^_&ep@0*-b@{lln`zZ%Z@TWbTYpw;{PwV{ck8apJ43vaJH zJx_+Rs87tl;YO4PXv7mgGuiA(4b|^H%kqDqh~bv=el;a@L}ju#abF8%4vh#|?SKWs zI$a=a+4rFqV8aAah}}1)+3oBE;at*4#lG8tRIx6EPwXc&o#2FmTv_^>99nW{aWflb zS8BKK{TRJo?z$@Nkm!^U>#Ul4a;Tm4wRX7+7dR!5h5GFqoBX)g6R#g5ugcX7ml2e1 zxmiTV;)0Z{H~YWQ{?L#QdD?u^w zOBScLs+OAyBOVi^g*If^4WWi<}5*FUy^{&(Zgx{4BNM7szSq0rx-ME zq`;MHnazlv?OiF>1E0sAVKWw^+1r(ZO?T?;Pr<(>W}9P1g!XAWihC7-ax(boi{qxl z>u+pmeSL)2CdElQu^+Pt3YYu6Iwc%vry2{>P?;0G+3WT#DkVxOj8eX_gy&5Z9GMD;G+ikS%LhKnD#L zsnw%0st1~T%(*0!ieDhB9AHWCVip*GeUJ5uEmSOGh7&><={Wt+;Lw~H!(4Tkgkp#V zn6wQJ;HXvISF%ju{z9n!0}qA9F{tniO54C4hRjPkKeE1|-l}Uv+PmeHqrS!s=k^D2 zki*X}5JudZgB{O5TcuvXRk4ibc2AY%SLI`X^{WSuD+N?13ooY2(u{}hT5zm8g122? zF|0ff^nQ64Clu9}nyoz}3lx3{K}_D}x(=+b`OU5e3{I;2Nf32mwaT~vn=oI;3k<&(&hbPq4d1XDbx}=2h5ybmi&wNLR#G-+q z(8WIO;u$kAv<32ikdPw~jkN*d(lG1{NE4&@9nEW%w+5>=(vmmA3$^G2*R*2oMldi9FYEd$K zW_usQc0jDt`}-8+?zhU!JytN}!8H|sYX5ZDv09ar?T`Gr0X)KPE%m55oLZH-grBEm z%D`-28zR4S0VQi6r>>g_Hc4geSHupxlQ?MfB7*Bd6Topw=vGvKRi9YxM;ehrrkA}s zg8K1QvU841$(ihT#h~sW$+j^_d>aok29FFvu&$2`x^KFV6Zp8K3s&v z)PwjkNK!sX$Am$|{KnS#8E$z4DSjlEAuglvePL26WFfD1pupjOBWk=l_h39e)(<*;6PcFJ7?3}2Ns z1s1a8-*czBRSo@n+>P5Zn-}=TDNwyUcOqT7wm8xj`*0un?|O_z+e>;d|5-necndw7 zKIvyYQu1pDgCkG!2j}z3-Mi)m(G`m0w=6wo%+w-?CU3_4#$Hi%K3X7`@G`&!H`;<- z;nOJH4IRK}*{><6@WvMY9lhNCqs)>UaVO!r$uSmIzsC!j^==SOR|AZ>kgRZ6gu)`y*&iB+kdRZ5kb1gkl_O4x#UJC~Bwh8cJLEV10=qRP-NK&q z9Y_<+d#4ZV6|=lOjgK1w??xHP=qXGP^0^3X@HMSbfaC3=D1Pn>TVO|FJm32tUhkwy zd*Eb{bwmJPN-3lktu$|`>(4tR)qn++9a47o_n2r@2rC+(QJk&m%py!QT>KeJ@liz* zY^O8p9_0c#*3K~m_?gwmWjKs`DL3&M7_+oZ*OE+Vw+t{Zp~P90AR(vT@R8gf?Nr<1*1A={o>^ug(B-p`amrABz*l5P0+er_y~r!XN>|GU|C#==eG71jpft-*cun~d zeV2&WTIc7=qvcOj!D{j6(PU@Sa}$y!B9xz6(+fe7oUi$cRrY{-dXh@lStB#VM6vD( z-_>w4@$S!cR-Tf(2NftcVMQgOZ`a2RYxcgO-H1GogSvrtJ4CEl<$;y#KZ}tNY5Uv{ z5a|a3;P|yGi>v$G!4w{lkGcD|g|E!_bPW)2F};g*R%FACP5H5?feahi@49u5lph}x z<%?8-Fg=KdQ{aAIXFm7tG}*F10ZA4>i(t!S$Kk?{C~BitJpJ`ZyG}>Uel{cqjZJ}q z(!~^KQJCpS?E89SoUpJ9$+LqYkC?xtma5YiPsbIJu>EEMud;+4UCoDloI0nG&;M;6 zeulm+WqN?u{y|05&WV!bE+ttsC0R!EPM+>g#IAIDcj~1INhqG@`?V91X-_py31RXJ zN}$lkB~%6F^;GMVl$CrYo)PpGLqrqbVcU4GqH3QL8aqYgZxE~JCeQF#)f zC|HpTm`x!0_rLi@2da&I_(t|fNC6!5os-)~-QvHeACu&Z@IDc>?`|p-B#y$6)mn7G zyqb$bE^B~<68P+y7~0lb@d^-5dJZ)DPz*kh+>G zrkaxF#t!C1R(92A>Qi+5%xuu9U?~Ta!XZWeM)602EE|acg{tJ+58gb(ir&%3{yv>4 zBwVyiZvTao=`cZ|)vr-wt6O)$cLp42p-g|Axwzf0WuViFZ{S-v_$#yfhT7(3qMT6F zE^TE9cGYLU-?rr&X-H(NN#0O`PcOfBpwo58kKld&P4(m&9bq9^nF>0hTg;Z*mVg1% zfn6RtDcR6<$F=9J9H@yjvhRK_xJ*0G=8LnaIC-U^=k$QAW|Iy*4>n&9k(QS3nwjA_ znH?EHg#t5nj#L}B6NcU+p`vm>Mn^;pPPcR9$D)h#wC0({{D=TFt}8kbok?OnY5!7+ z$?As^9q?UVr(r|y(cz!K5=r;h<2EmeA$yE9w$0*c>PJ{_>5|1B+^-8qP`=ccSbGPW zd2<)mFpn!Cb1+$xC6)7)_YF_z%{0qy)I4c(l{rX|{Iu_*I44%HLz>~nTGIY%YWC?< zYnl%6%jK4tqJqwCQ<}-o?=!H)Ij=cvHSmI-Z!n&I+Bg*+F+Jin;a|b&oJF2;?ITy3 z8~+$HrO!sx*|)gnzZ?2~z(va#lNcm$|4<@(EcvjGjJJpsohEjfw&#oY)Fi7nw$seY z`IV1Cpq+>;Jr_y14&f>CSP?B3Zwt#{ekuuxJ}$qxhu=lO=JX}m2`iiVuc!&6`Y_whl}kR zkKG!KXyv+s*A*k&M4B3L3{|8B-xmkHI(6JU63U<>q1J%^C-N~M?OlP=6^_`^7kRXb zv92}_gks}er{}NaT-?7HKrx*d?o8^51T~8$PC>UBqNK*_xP9}!{3Hhj)2Zt(v%YZG zbkZSV^ZKvYu$TDdCz_zY2e5w*=j|_6j6!$+RIT;5cE+!Xjl*KN#76xXXd>&XT0og8 zYYD);b_#1>f1le0$5D!hmB)?qRwzW4DX=8ynamaK@NSu)it^UB1!4V_aXOxD5U9=8 zcY|rmPbSzWy{EG6TZ((`73Cu&_gz$%xuI2z=>YxN#W;^TE+W_!>xV$8RKET6ZC%x; z2E&wywg@MsI`J*|DF!N%Yp;i&Pi(8amFN}5S9WRox*>yDq@8Q`atGhr@Rej_WJoT4 z+g$$C(HDVDgSH$g$2DqDi-JFr98w(aP2jzq{lDxM)$6ck+G{DH6-(!C;^fb=4Je^1 zF*F_hO7bw$>m_ODvb3 zd|9!y$tZ<~frVM8a$@m3qWOAt`h1PfqxU}b$iFZ=Y(3AwfzU_W2u8)~!LVo1%9%xI zq%w6jw;rjbgALgUBT9>q_SH#){?~vi5ha(#aS;vEiNEhjK`Vjx)s1@so6Gcsn=|Ex zxv6C0Sa1j_^;O&G`HE0|Y6pTyd$IGEZY(@MjBy7U1EDbbN08^IB)eab6!wPgQnxS* z9X``BWx0RS@3aUBnvPEQu{pJ;lE#hp@M<}wbjsp-2`OY`R92aE-MF5RA!qRH!Seb< zshAk2{C>JhY`VuK*lSc@YQhfMM-g=!(L{D{R{`RHtcVk1@klBvNzmc5~RpyS_7ANMe1N!9?|pN9ex{397x>@;V9B| z*z%3~X-->Y>R37tOKN3tUwuEO)cf-EbzK|^N*P@woU&R*+_kFHkof@PT$v8rF=SRc5@3?}y^LakkvI@us&MvSAjLXy)s6y5(lZk=eEq9<4^YrByJx;Wi z)QN4^d&*cSUNu8Y!tg~aQer0nIsgPs844(WXrMdJL;#Mr#(as;ymGZE5199oz$~Ab zkPrak&TE$9$pjHZa^gYsGJmpz6BzmGU>_kTVD3pVU?e2Z^ozrX3N#!wOFUFJT>4^5 zzB145!_bVQm~F01?sikSYv;^#&p-JHWB|i_R|VPRtZf zAC>IxdOmxW*0vs$>lBX{U|Zno#>Dhv$*O3RP|_WCxEtzAmH*@aX|&+decRkrk`So; z;3lfRWDobaD>9t z`UUSgy?jIqnJ)4aaiNhv)WpvqvkXR@#A-J#Br3fq6nt=khu&0~!-61gNf1iF2}Y_& zitm7V1RIdVSujs_eRFdYlq%S5Lgci(>0jdpImGln0RSEaq1fv$t6x7--`czLH|Ps$ zOy!uuc7+Rd-x&Q!EBo08Hm4p_Z%R05h(O`CISSZQeT{H{03gNAW4Jm8)Z z9g-f!N459Sbf28`yX+3`O)IKZb5l$FgNy>7764V2N--QS^IK`KU7uUOypy z`DPOdor<}0z1sO{%A%>B1arIKSBBJJ*EzY+Zw$QJQ7iI@9zaD!hpL+>4_3y67^^t4 z^Wb-o#>=)8onUiZu|jO&ZaVLgcr|V~1W9?LKB4c)M|`qakdIiFz1#9W68G*w%3OP% zwbeG*Z~vRv;D)vcqipq~IEF(3)%84i8soM7;3 zY$A;;H6uzb&)t!AHS*_2qX!ZgwpziUI~0Mq!!y(!OzNUjz_9(@ zu#%z+2Yvv6h-dqgIb%&zc>PCi$MU3m)dY0q%BU-z94_>6TK&%(7}9T(c-Fx7H&EzW zNlpk!V{^9$kJO(K96Ur^&ouL`Jp(1s|EH{6sS%D)=lv%tN3P zLZuPGcCPfccb|RcK^`mJO}=|>wHS4Ae77e5#qYFidXPvk+dWo&1YGPM(6f2y-lHpO zWe8KXTZc18Mxr^0*(!Q?jv$|lcpiiIEQ`#YkY~Sz_S4qr5 z=@aT-p?e~v-}hD|QI-C|5n&_$f4S5&dzpCrWy2p*PC$5uSKA$;iK?n6>pp&;+Lk5u3D`}aRb?OLce+n7Aho14(^+PS^8S)56qwJ1me1R za*aEe&f6P6>}%b@5`$l(>VHWyJwli1^_m^8?$%QrJUdpx4y(WHdnjQfRdlcVhly?B zkR~gmAi0)PCFMwRKfIDGtO^}wgV@UwakWj}fz(>ChJz2BI7$+Kcaw+W&Krx;0PY|7 zC7~d+I?iY1;DOT)=t8A?{%@3;?)-oFTERdZPcTvdr0W0qTcJSX3^0-)pt0+s&nFEP z3{hrk#VSIR%4)FZS7U}qAIIa;{Zc9L`ooQ75YOjku%d_cgxG=!@cXRQ)M8A$yw}eV zdEA@$KCp{-23FbNKR=;sQ#j~qLEJc$dl~F;4haN=)L%_|;ctFojzwSvuIuxuXOFcDHC1gaVd{0$Kt7av(LwM*m zGHhx$X|{Y$^oUkgcgIL84hdE%iVWxirF!NY@qCmJlPy$FzGKvbO1JENPq=Jcv5il~g*1Fa`lCIE{UCIr$hfDguh z{w}Tn^Iy}gdY8ILeJ|*iAObQ*>ac5imw%U%`3S6p;Z#=^u{jLuwxa@A;U*6rI|}~V zSt{?J#=Pd7WNg^43v+mJIVVe@wNN^LI#7QvB*9!{eGzyRi|fH$$p>G}%A=?j6aw8P ze91PuR1ppglE>U%S$#4lo+(>k#sn(QzTdN>j&C~|ETybD!-J9eo7i45YZ3+?#z&7CI?*(DCCr(@EJmKAB^yQ{;phr(9>0Mf=AW z`ige@u1&)nryuce^ChYu0!cG<+FqY+nW`dQFJoL}{R0{Z?|?V5XH1am5By(Oj9O}k zhuA$E8Qld0OTPr^m-F*NwLDqb!BLA33Pz`mnyF@@hPNm`t&)R2lM5gpurbugBh;6y zg?Nd7PmFKPjF4kbHI3~t!>ABp^}3}sj5T^SNBv8{Nfo@WT4Nv55n7hb-GebCs@nqz z^G0`&hRYl5dqgms@q3O8TnbzzndIKI1Hm`1f1|jzUfrjY9mJeAcg9iYhFGHm=V=qV zZqzMrKNr`zi`|-L2Ll{kS&>pM-WO!`d#PMJen^(+f?fCRA09U;l%m`hU1=r#H1&w9vFiQR^x6}hHq^{F-#gch_w#@w^OI;A~tR)^n^VhUcf zc1AO!t^mkh?!0;ppT;x@wm6a9FdYa=D3FnZzI_Ad^(EQM&^sxLl`m=RwKonFQ021l z@9SO-fn&Mqr?kb#H&4USCy5;-n35!Q1?kZ${ByTZGF@L9Qj7UFj-r zzJQUQLrt_GH6Wn6JJ@mQZw)ubs&l$Gf^lF`b5rsyKxeDz3@j z-ZTC1y41ssyGmD=;!a9kEjU?f2~J&ixb1wx6N;`j@?l^flCOq?GL|M3riv>!C8Dp8 ziecOosIH%)vIU0wIchIIh49b4fa_&)gj{x(Fycg;N4jy9hz0yuAlE9cPBu@@Dfgdf zXq&aQ_*C9EI17X3+NJMDv3(av5AB*b5{mEl7#iL9%JXWajFozIy*o~u(ZHUX`Wc7{ zvFrJ6%5U_wav_pyD^||iSWqJNX zVlLh_+Sd`GSpd2Yq#;fJDqmjCMzovYx`<#=#4ju(?-r7Nko^JK(WALMO$W+cA6&1+ z-Xf~nG|E&L@}s4ko%{ekD*f7^3sAvKZ0h0|P=84u;lji)h>yEdN`hm8*WU0Clo z4e({xDGc^K1zz_Zu**2To0fWOO&bHk{iB${^#pjGq}fK0JvA;m@m)I={|~xcUy+V{ zPa{w|n2kk!j1^Tl7~{GSQjbmPXdgJS!p)uEQF*=G?me{o_<%_J{OVd5?!C?C+Lh3c{}v;sy`>ZsqsrDE z7)Md0%USuAWJ%ksLo@adUT70uR;X|}jSHT=2RD;pR@BF(l%lH-lEeyAd9S^FX7z(? zye+1l;vJ=EZN|vP*{~!|9~R-z^ht)~zy5Oy@DIB+U$P=!tehCJE4@XYDrl@TGbls! z6Vy7Lsy?~t6|-Dd(a8Mo?@237C1xRQ&t?F|VE8yT4T1c+1aV}WDPJ+jAeufrjPNcK zNgtrYX#WO0Rh(wtgwe=j=^U@e8KXG6U50tp{qV4UYN;%`I|9XLqcN+tCLZ0e5 zg{ThsGih&1h3KgX${VAJ#b0q>t#A`KXO5#0*pM9AMfQ%<=QB9bzza2WIto`W$@)R* zlxY~>ZbyAOc5Ti0`&vy!v-FDSRt4^PR5N06;%oHGD#F^#Yn+zM0b}3>&DTJ;$oZ`w zv}J`tPn`0lbfA~aKJu#Sn>=>i=&mmN$&Q9SAlgsoFA9O(O>BxTj7Ill!nWX`3p~f7 z+v$cB?l3F;I$nUm8!zjkGO;)FiB|%$KR^ocP{#-`2C%c6GMxo6G!e z(R9Yq|E(`oSEoG9SBa1T7Ayo;*D}&3n<1Ho(xU#71tUcIxwb*v2HVad zUtO~L+}^zX8bX>r3jvqYC*QVp`tJ(7ENVmtp6cKZ$pvB*CF%crukqrTeX|!9?GfAo zBAjiq$U0kppo`-r{G#iSv^>R__EujDXLBxw(zUEl8GZDJ^#E{N)Ia~H>KhJvB5qn+ zo2Nq!rQu}N;5n>{j*Lj%QKszI=Me}xTh7{f`U>(GGto&Z9zJKJ;ub#h1pS&?8t2uV-k|9n;fia8 z4>ly+2u7L&H|8_FG7)5aBOVHT8YnX=UcWL4Lxu(hB&qyvXHAJ>l_bfD!GG?@mvnDP zAWs^+4WBa|xMX4rr0^c7j+>jViN9?qe{Zeg!k#=uu(>Q(WO`QmOE^y$Ti)Geel*6& zYL^>&3sfF>j7z%L-#An_*QlHxHQ^x>NB#+uVgz@D-*y|>`xqfBOm1Ybyg!H+O0H91 z4-)d^`8tANk9{<`k7S=czImy_s8gajR zJ-Tr+MGHG4Qo*g-$II9vFf@XTEVF}f@sO94@2IN6eIbk6S9x;U4Oog6W8Joa17QiEF8i6;}IjTuAyH-FM>M>e4V3kjVndGa?O1#)g7 zOxc{#N0ONr|F_vfzoe798FCbIg4caMPShH087*q)pA=>A3h(YuA8FDP`A;yl&S8bHE=ED(lcpt&Y$6v0_StGycG!k;5s?8YCbzAsH8 zsl_?ski9@LPdD7uM#CgKbFdc$sELG}snODP6oiJc(&-~Np*m8>2 zsD5+$1t!tR{ZDqw5X(SQ^-RNexY@^8++H;9|{AD(p|ODWeJiF{TQ$-e4)y@s}RI3`nSmXH6`C|kFLz?YJ*MOPCl%qWcvSt@7_ z_8byi__l1aG-i-NJVQZDWPhd#Go9KJv!RYseK{iO73~$GxtB?PL?k)L=j(Qg>G!IO zc>_7G{CcjnuL5HzijpE@5I4Ws^SD=p z&21Ls@YCzu;<&vT!Eq!Y&I%mmL}q}7<>U0tULYY)?dl9u*LCh8f(j;4O)N?s5t)aRBxrfeqzW;2Go;95(o8dy9 z%bmrJStUj{3M(IT*Xr9fSnWGkPNqhLEb0CX{%d#mj&1{$5XgtP3)hwfVk+B|HwGUE zqmso!3f4Bed-_gbIcr19sCd;_OgU;pnc4g(kSJ2~n=#@iEIEo`m%9vosL6jF5)7rU zPoPHLvTIr{0DtJ?$||Db_WnXLwbIn%cU4mIe6{u1Kk06W+NVYNr2oAYa=p?O``FT$ zLR2Y_ch+91ZD&sFW*`j#wCm`4rR3_^#e+qZ-z-!m30VXP;xQv_4w`^b*>SA{@3PX$ z7mk@$IXDNi&MMF9^OaNr!x+Nrk6_qQk=SH^hb)qHEC*zfEhszTM>z6$7$T74e5`To zgc2LvSzC6u?@Bd^8<6~D$rA&xFR+B@vG$X3%t={`R9W&ml#^k^2}#V=Y08DPWD3EA zs3?3qoj@e{t_w>ICZEFo-Th5R-Z+sjRyE;?e?O}`-v8*X9@~NfZ13bleVPPhB4uTTQphZ;P`~ry{yxw5IiBbF|9=1bIPT-V@%g;RHD2R9&)4-T%-*(EOq7N5 zF|){k2=l{(ZtTLo_jd>U6m)C+MOhq=!x)cvW+CkI61&gglABhDvu*}H`_wFp_UZ!gZXS;f|lH6HY!iVjWqF#%E}h%p)ty z$s(vj8u1>>FstG*HMn(=De1KBZIMC4gEHiiRZX{@+JYuXBGuVWRX@a5D<6NvGWTLa zoaMHWKx1aaozsuCzV9|Q*prg>cbo88gUG1<9itMr=>GIo(cKczrnPB(=8p#L_4j2$ z(5N$H8tsMU(6N+s(hTL4VY_kT#*>R&#g<_2cou9~l&TFVfs>lUqrC412NmuJ3Do4 z2OD?A-Zz=g-F|kex}jamrQ9c=WrNEjW?PQetF!z&^6Zm5fBkT9IrDt$Q?$dR^xz=m ze=d2ZnTE-6&$w$+mK@oS3CCzRFv(ur= z4g=TLFV4lSa{uSIt|Btlv;Et_6!vk|$Gv4FsnfH6F?q%8!Mgx1G}Uz5|6%m!FDols z>zo(w+V7eVzJGW}%re}xLq+INUWxp~kFHhdL8vzz?XlQYfHtFJ;!0dVM(#pYvWrX! zugTbMLE)vYgo~Z?63iF75{8zG+Qq(Rn)h1$&QoZ)%Sc%8)WuCl<860rx?iMPfBO$} zl;e56;k{=>qo*!?miz9f^6>bpey5w$=wey%&A<8I`Y;|*k1*|y(0-!(h>x^HY!jQ= zf05;iHOrOEec2Hms?+Mn(nRh4FHh1;nM~tCZ1}d+=dpFcxZtMi_A4vt-sDTJN2kX8 zS-;^h+2dzFFbZlt@j;W7=atgdwot$3 z(oVx1*2+kk{mOd9x0#h=Z{+jG%=WZtOfBeNTv=J!)0m``I*s$=eM~oIqrcF*cGe5s zuRp#%$g8+i>j_XD$a;x9op`m&(!=uQVx`jqL zWB+lXm&*mV^N8>St~?~QU6)Z&VO``qY2_7iDwp~E_8i}5d%~q*ZdIv}x(idLT0|Ga zr-lptFJi=I>N?N2g?fCei(zcrqaDL+Qkn)Xn`bDtg@}~U96frp?(SZ- zx&7Me>h$Oh9<-W?f6JCFFU`!^MKv5{*EbnO%kb!`3!-8!q2`F5!O6BJl1Y8BdI+mCl-Vn)`*Kr zbVPTx+KF;!-MeGnlB$RPpzf{O_xl!ZC~=mNhPRtHhsDHjK>nEc+VY{$&fJ98%17j$ zr9N~i&`a_x5%VRF9wjGSai_js_mafg-_;cslxo=_djFVDt^;eb1k2GS-uze*+U&}& zm1){y&YqaE`u(aC_x3d!J20Wyv0iZA)q=hTHGk#T0=JpXL@63w%x*T@w{i3C2oE`4 z9IPf7C6qV$C4?>rs+-J#{pj(wH>Ts;5}!Pw zQBhHGaB{j57Z;biQ0`BM4YvjsH54bGo1j^k+m{s2*tNR4I(>~OA!EPksxi%lTmUyqZ z>Gs?lo7Tki^mXt=q!AX{r1lOB1irnuul39`i(bEV`8qts+g@x{D#w!r4p5mNV~mQ3 zSeBz7mSJ9lHcG$0i|qe9{}X?sP*$v5*_+d9RDtHe)*>e|-R_oi@X=lx$$YFW93yc` z5aY`zV}jey&(E8j-r6N3AV7;wCpWWmY&)Xzcq1yq{P>vP4ClOgJA7W_Ha+?Hji8NM zeiuDFKDX!CY!?*#kb4TP>py<{=r-GXb!*1Tl`GGpk;90E3WHqT6n(9e*tbjp9!w3! z5J31Y^ibYo-9$51Ji`g|_xkngbcbHK*+br!8fBH?7_HYoVLdK4j5&f*3 zMJ#0e13C9oYiQb^KJB0P$(#Lo(GObLxnVKNK~q!iC=uf|(ZluhyNKV$UqjGctko{p zqS4G=#kh{6@7vi#b#jG3^_rd;zvB5HZ_+a|%39J4OmVfx_uXklO6N0Oid7$$Z;SK1 z0+KZePB`bTkY4WW{=txF`71^0w^lE{_~6E=-=pXkZ`M~CJOYYU|DSn!<%Y z9qDr4ikpfre&Lb%yGdtsLwB3$a-Bfu<3lIbW~kFQ1phbJNPPc8=*+*s99 ze=cyRq5ojS(zm$rEhi_Zb?etB?tCc#Tf!@SFYmTybDyZ{);BvnIsP?NR-3f^toE>N zXMW*pN$bCiE$mTCL}I`;*^|P0YpjOO{A)(u^aM%1fB!z+eac$Jz|*s++l7mZD|F-T zzCWT5sC)H$V+|P76n9*?vY>v4;h!lm^sB0=`5bxDsIl;wlSiIim#!aUu-e{yz0s$q z)-dlg`P|arw~2g~F*^K^qAt3V|3UXZRQ1|*w!QOwLKUngazkj9f#tzR>EfFI4B=^P zpxd{v;@Exl7>SH$KN6_7i%&#Zz<@1V+N%l{?$jD}H*VeX3uE8cqAlEXu!>LBvi@AR zg6+Rvr6Hat^wma3tM?nk@+Px)Yw|D6P7P1!kfGtjldp2<&z_6ePgX`?-Q}_@-l)h( z?wvc+4rhpqi#uGnQ0%&8@7al8u10m8%FHiY<34;Kwy)9mO%Km-Q}T)NiHT&*b9h^a zFRvJVZzcxt<|&7z{*YGGy{>$vJj-{r7@1Tut0BAW?Z?U8O%M^8getnyPrspqS`Lo5h?TNG~i1re;&~aiqEl(cY!ae4W?~s=I z@rQ$hv1%IsY=Dah%5d!`^w76v62^eOk8#Ekm4P6eqTA$@y(%=l#qs7C3gz^ z<8JbC9>jZKWp3H}*e?6iI{pi16I2Vo1od@1gp}Bc$uB4=7Z^5_w73iVvk9Jveh2Mf=Qy`#ByK#X&Dt9`j-iXC01Qx0`wMi8~_O*9#P zS|Ik0ZF|IZ{jWZJ%V*4!LkGpoN;icpoZ*e3I^DGj$?&$HF&FU=jV zu<#o5>d-O_DEhdQ=J9>WkMEbCH1_i6*SLGueDmhbQ>>EE`?gPgWlm{ew=cR}dvlx3 z?ejlx&6<2v^;($doQ;({QFM^tX|_eNXZFM~vW0+yLV zf$TIV15v>3&ilFviD?&qem%pIaxE)MY<^)O=KzwE=IVR;{ol%F6VH%smDqtvPl2hv zf>_vma-flB&6?_l1ApmZVD}Beqv-z zRlXXTR2Y=GWcayQ4g9ul+xB_*V^%~i7Nq(2mbBUVpAB0ZjEg7VtDS9n!r1LKb82KR zUsP0dylR87$?SOV+2*H_5tF$>cU;WWqQp#4<257S`h;=V%Ee!&(sc-CS}Z+1{o*f`%CFdFnfAYH+f%z_syDHkYJE5R zpqBmDRwTIhTlXu!e{A>${2}FsHrs<8d)xaD-k`BLD!X>^hFX^uH#Q4&eJhKI?|HyM z9zj80sQige)%Uv4S1OW=cN?wd3AK(aarLY1x_fs!J6SC}KP$-Z>gxh|7-?s=eBfL1 z0|B~znrZK)*MF_IT$(-sypKMw-$BFQu{U;YxUp)Vl8x?1$-BD*mo8F{i)iR1P#09W zKEeyqR(~t|&pT=uz8Fml^yCaBzfumybKcdJx#{#u&yS2ZqYAlnlKt^DEA5r|$mJq1 zhB=RZOimu+3CiPLPd=l)cmvOeH_{ItIeL`5+yh@+hc@i6^5OFRiqvcg8)^4-hbxo6 zIPb$42_AQPqiM*W{ickEQAgP5`p33=|9V#P!ixRTjLRiuWXQE|7?E+R)OP0-y3^oG ze#1Ceh0pkp)&&L~OQ#|Jj^}+D`OP0!zm?Jao5(MC#K>S)b@7_&$c0gE)O9c}M!S=E z_L(&GoenZTk1&3{%G?$o!)r>OO2HkkITv+Dn>y!fz0-B&6&9kePAzEMW+wN$5p9OA z?G!&HwL>YLpYaF3_w^m;)NheLG2Zw^{+6~l99VY$O!otMKPz5dUfw%6n7mJ1QgTzF>)2tWJ#un# zyDVOy+BS7-;Pc7-h)h;cj?^vg4Xu?Sn^z0MAztT}e^>0`N}AT{>FMcaonctE zeE9?_8NXLvnj6VG`!RE?%SgLF63U{5=?)ZAR3PB06Ky#@aBmYM&idEz)c9vGE@rjy zoC9FhmSwq?_zzMF#^p81xQ0tM6rfTMf?B2H$0NkW0ojQZjW09u{&jk|vw%r0N~kPa z%rd_5EOEcJ-fy{EBDC&r+O5oYsJoI&w4iEj=HfzYULCTt9GMpt1-M^S_RQdiRcv9@ zB&p8bYts^+oz0we=I#63y+~#NkqJQk>v!qzLTjqo&Y)3c-DTVQw~)2*Gyp~kDcwp6 zhlIny_a&G9dgs4*!6PK3-r+Lvj(Y_)b$RPa_YlL@XQ!wrtJZSvNzqkX|M=TxQzzGd zmbVs5NU`O)rYD-pYHAe-Bht-l*3AC?-Pe|3zFkD520&#`UteOFhPt`}9>{sRcWi7e zB|=l|d|r~1or27Ky&#qF@_Bz~b{vXcwfAhrR2#?4OFo=26 zEeaIUA?v>950w}h4;>G+L|@oIx4juY$KDhO&o7aaA%a7&% z+z>|$-4g*a(6?{P^WPlIrsKxrPY!=%f@V}%Mdd~%r?iWDVRww#PR0JgL0?6l_;mls zkh~yKvU+$#FX5{U8+TE5@7^tyMf>bjAuDBAS-$<-pqm1sbvaUaTb<+87^?QREXlO{R zdv15e*X_Ke9L2IPE?1^Hsd%hDHLe?gMTpZx4vwDM1cD#?e*GmuNd~%&Dw_-U9ZeITwmA@Z8qje z9b*%g$T#s?Rzo`$eE`oJ;yUU+*3+&Le=&BZ@SblNeZ9-P?&4Lo{Xaz|R|DIrc6K@3 zeEQDIHl^5v>nDw$EdEvIm9F27L*1NTgCE(^cFC#JtkC_|la}!8kur+LfEt*=&G@?g z2Z(jqw{JSjcsg-ogO&HoXXD1bgBBC^GE1iA#C)|n^-t&HJs&jhyEUBO7EyX}Rrq$l zW!Cw#!r~XKm)yWL#f_UZqGAK9k6GXDxBCxpLUi0kK~(?ZnM4b{UN|&z>j5J?S>tOl zCcZcD|2MxZ8g2^}^2<@q)i*y%%YS{>=DZY^=+Us6TI|XZUHmhqgEMcElIh zM3d_TPMR|wrq&b`yPol6L+~2=h-@3$tQ+|Mrq}dLrx!yMi%nz@Zj}mOkrGoGle%zb z{VnOg#xj4CW=aOAp&ip@2* zSPwA^ri3kqsP5O{+t|o+p80&tD>Q7m#dQ7DKhC0m@k=)ad){53r|4-jIt+-jiWVfw zepuXPncIcRUu~)9?GETH-r-EQd()Rujo$fLga0xouVQ6-e@!3SGbE*)~{bbB%VPXxRjJU{VrBuxJFu9I=`?`o{rWXNIq7%ro!tWu!?<(dc(T?&>vOI@f9C9c z^EU6bX6}Xy(SKVLbFJh0D+^K2>5rDMX2Jh?4FMjWWeEuh{F2i!g28=#X6mt0&3rZ6 zKIo;JPmPVc_Scb02_`kx*z8G};qVwG?MBcpJjwI+3U zcBkikQw!(XgDVa+B{S)y>Rvvyd{xW$46dS2PGotBCFqnK*pBzP|o!NAAuaKYuz7zTb-Vl7uWusz&1cSNe0dwqa}xDOR$-NvF>pJ9fB@ z7JPdexFL8jJ5q*yhn$|)ry1m}PKGm+8S@YfHU^LyGl0}z^MDSWx9Nh>zdWVkKk z)tfi+&dz%%CdgnVE(|eK;vYO%f&TOa6-Fk!`&`?u5tpRHVdY8JWP@wCE^(lh?bVw% znS_ms0{<+`5g_U^5=wCwEJaj()WA*B*B3wfHSqIW+@-eB98=Z@R~iUqN{zR$Yo&SSAUmlT;Q~_<{DQGW-ZlrZXoFzUk7ZODOjtGVrS^l7nf&~M!wqwkEfTH zbTfqeqEgSArd`kcRVJ893-91)`Ui@i~YSOMpM+cEQEkH~Jco?WK zrPy6_D4yx|Xe;rOzVFtznv!Q}i}lnC=Yp{q5-2MuDSZa4gPh5C-7=&C<$kM~ zdWMFA!D!f+GyLlG_xCTumoYxiTY2acsZ`X|h-v^BzFPZ<6DRz$va&YHxMw2?XxFv3 zx8LpbML8=ZEvPQ_+{Cco-(dXLH*arK)g3>n1Bc}+WX0CZy_Fc=_c{F&{Y?DK$EokF zRrvQpyL}1kc02X#efQ$-JzvRo!5d~PQRy0IL@tspT6T7J%sjVM>)Aa#;^I0Sl1>W4 zpGu8A!Cfr#nVqQN%Gr(hy8Oe36U-YnsO64y7Gw>4%zFG_ClV(5%~D$LS$~gLNZJ1G zv%Y&x7U8SetLx}w3FpB?sRS2eJTkpkrUp?iEyb)cQBi7c<378Xv+dc*Wi?)K166=` z6@hyzo;~|SSzVo3jz&`}b-f%-hWCOy)j`8z_ndaG+J2{rulMYSve&ZhQTFrqACxhU zRDEcBB3Y}vy819|t=jK|2M-jG=LYFzow4e$zI);{hI9j^5*umj$m!Fk6RkcxO2j5z z+$1Lxfa3KX`1|W71Sw*1`t&B}PPz@k@pvt_$ws9KaLVPi zDpJ<%;F=|*q!N$D$==yzx{Pq2c3m&JW>)Z-nwmy13L=4Yb9YaK*@*CleZ8EQm-pq< z%eEM#91IlP{OW^K3CGA)Np7{3w?4po^qlR;F~d}Qtrw$vp75OeTpdQBMc7PUt81a5 ze$SsjuU~!<8;+VX+>v+GfD$b)gs4z&ji6GVbt|1r9xpBTmLFpa5xUVguXY=38rv54 zRQ=8_s6c+Ms+otrm800L)dNN;Gqki{+16(ghsFr&v8b3EG zK4xTO_s)^(U{Ygzdit@ki`3NAB+UWazSaBhf~zMuQsQT@HOVnGh^nYxj?A3aX7+PU z*=`}2GURFVe(m13$(g}k64KJ&Q_s7&9M;p@1e!FvgZ+2Go40Q-7Zw(FU%2qFjM)^x zWCfi1E=+3drFfW!;;bwH3d`~rqMXkM|JUpvWRj*=LF>bUU^#vtc{BGag5U^%*Fp>Rmd5%+omZn^~bcw#mW@r>EBq24Go-mWF z`za2;e)PM)`Dr(E@2IkJpiB9U!j-F5EvJ~7nZ3)k?U<_<+rFKKLfBpUjl1GsOUQ?_ zv#|xE;Fk>m`{6iQuXNWjquRjJReuEdJ72wh3&_~>GUAv7?G&AM%cT7>GC5gl-8;Ft z4_P{=i8t{*e!XnjG71Hi=}Py+ZTY@GGcH{tBJx~-F5d%|%vbfH9L!>9d$%sbBbttM z)8&+b&R`k}LSgr-l7f#aQfI&W6;%FTse}vep#cG2gfjRCpC3ImmX+DJOk!`QQpVHD z_z^#!ZrgLz4RRCy#qTV-e6rWwEPK6^BJO*J%`lN=s1gLJO0 zu0nsmFgcGkp4fH`7}_4I_mw@Lb8TnV{rT;|6~M6h%4k<1*8iv8Z(KfR+CeUHEaygs zCx)7=?i{!z+xPjo)p;8PmSBgjB$`FKG)=v{ypZqsnOZ;3idQo4X?@S)yp#Zt5ltTgtTV#G*g;ii3mNLW_zNg^VjQ- z9YZ3GppZ}{6e)pMxx6>VO1rk`*;`sZc2&;F&ekF@$kfHpVf%Jn{9!TTWOsi(mykiw zb&~`61>(tP-pp;-d@s;t=ko898yOhp>Xw{OXoa^}r*F3sdc}&+Q~0o}NvD0v+EK#DGU)rThuMG~Qc59h+1f zyVHAqLMfC(a=kD0nwNAUMHDz*Bmji&IF_u{R&f5YW(~tykBwxj`%o=^S9B4^LvlXev*+7rBBbykCH2-pyg z1Ku34oQDq|4*1gfRO-{++}1shU(YXiW;pe$s^8tSnyLC)1ul2}{{3*cA|5`z6V9Pd_7#tD4os^Nqd*#a066!OjzjwFmM^bD0EsR&J-M)SMJ84>K zeEtvM$`4jYH8fT_SO$JDG@EjCaf#|YV>Nc{=OqhdMWRQYERXKlG`EB8nh~{Ts$X6A zCeebylKb$#$Ocvs^bMy)<>b|9lb995JAvrnGkpBjkPtdT-XQEI#|K1s#$AL*n(98@ z`r?93EiWqt;7!7L!Bcb7BfXzTPT9tA{~BOoX7;P$^q@EPnu>ftvz(mX#v>)ghPbw3 zdZfeD+}sxvV>&b1bvV`>mdT+X2k~|0GfKOT`oXR^IP5xnjWdjq_i$&cBWzDF1SX#! z-Zmzk~tFO`J|=oO40JgNSv6?n~X)C;|)(DPdA}BfSBMSkshzOh(#ec%$sEFHHIQ`Au^f zJO8*;LhE(%f*=oN|BqL^y4hzLzc$2OgR&~yu8R$W$h)42DZ~DgY;Du?uj|wnVIy)q8Mk%*yyneK9Z2On`~e(Sbyl`i0AgzHMh`!Fk8Y0nN6K4qm96;KWEA$jHfQ z7qu8xDKzcUn^$h@s|vB<=)gID&O=SLW@d{V&XeAZ*&Dk^63@e<2I7glMBo$7j3p+B z=c&>X0D?DT3|abEqI>24*T-&Dm8LF&WTT&uyEp<0q4=ot%Ol(D>FvD?UBmph1Ah;> zx#jbDLqr8)v@AjXDzf(M5nwk1%_$pFQ&Wio1mUMuYjU%-^Y<18kg+vSwNsqK>7aRD z1{Qc=9pmM}50A>4n{(DQ+kzdy*dB0&3MXJ)3TVTtD^*rJNx7rr|{p5f-^)-*N9)GW9}%USSj`qh7D;uLk! zr_if^Q*Fk*#W=-E-W)vaU7+&g#+!a1Axds;5-*$wuKi8xmK2kmzR;1H2@2&`1@$L&L>AKraJ^U-};h&S2G*l=vrFCJVj$Cw~tF)Y9(uBA7TK+>x_ZfZeB}K(peTmX_wSB1I0V ztNYAoSa-30G+7nQ!9%52o@xoC{&8YSO-tKt z@#?z%fekz-0K{UGICEvvHobcF`~{7ej$`yeN4=4;R$br{%PE+0>keX|ARzsWZ8GpK zVfh*k2?_;C*EY!Wu!#xiLQE%-BAS{e)TaefuHU_F)hxqpLB^G_2>5409oWad~YZJDR(NIKKg zvt|EG=lOi+!Q-*_H{96au*g&m$mUDFPC4~_%te8Fg&oV;(E|ImN`l3CqTNSjt5uI4 zT}45TVcmflS+Q-~Hsq6B$bQMBA&z}|>e9^l#W}<2QuSIhFJprw1)vA!JoUmw{<{wM z?AWEhDVt4st+y}jlCG;BGMBVd1uhrIrX86J9TO$X#W{|4xdoAwmOW0|Kb5*S3=Uv9 z1q(FM7F9%;gH?q6gMsgX_6~ZYXcsRW6$0~byk+awML;6mw8Gj@OG;Wg2`9CJb8(Ao zXlB$C_-8{bvtN5&@x;EB0=HTJQMW$%RrEk&-#&!I6DuL^*lkuxQ~Lr3x+ccQQwYiV z#Yqw#C*f3U%1IF#?sT^avs<@r4HUCvNDU4Sre|fDQypelvt*K(#=#Ku45R37aM`MR z9f|?qDo(h+Air{xivAO@6|fFA)Z|bzo$AB&@VQv^&RWlnOT8&R&}72?tZBb3x%{2% zhFrTYDV5=38t!q>uK6w_Z2anXj!AEmw>Yf+w?hL9ZPMq*5*wjAoxSRo00UIS@z?L! zHGLb^qtzB*vFk2cee&c<67JyKlmL>-i6c!!L@TeO!;0K7DXRnmZ~}V*plQTP_uxE< zP#K$<;g=Xe1#a@v@>~Ic(N5 z7ip;UaeQkM(lrcxv)%!aj78(+bPQ| zJX;)A6#2cgv$N;x*E8deA=+ErJA&Aj$ucb>*$P_Mmevd-V>9pn4tW0$AKMG)K0zl% zBbJ&m8hbjx(8y!#$I@QaS`I82u%DuiPE_&cv^b)ReKArY{i;SLITAF>Qezs3uo@>tB-mAKcPSjdg z=r`HLvh(Bea0>*Ebz7KfZOlBa=K1H>H>nKZl?2*`6zl5Wg|SOS^Ft(kk*cTv(^)$+ zPFJ(r_uM=xTNZcePY{qxA}6>leCqwhbYT&ZwIsg;>`cz*LTthVpAU^fH57Sog%`sR z2$2C(64vwRfdytDo;zSV9@x4%IT2J%APDAn=lc&IGGK+;+S`{=0H}-)r93yoHIU{c@IEM~)n6pJJ)w)`>&p zWL|bgd^BuUceJB(# z7P`>M6XhlLrF94^sOwv|Z+{;qUWCFYVz}q3u2;9)X9hsddWDAi`r-5Cajr@PZUYHd z`9@e846CZDRLtzVUOdbKoP@@J&iRs+L!XC-=`Ah4S(^{ZIcjnThtM{*Rc*3+rZKcI z8x{EP*Z;3K%bgoG;x%C*5=kX4FJ|Hx1+AXzmK2%{wq@}4_4UD1ZwZod9p#`XpE-$F zT-pO<;jka@-VY-VpoHJ*Z|_wu3I%pHvJxuf&nV=Gs!H4ziY)7slOW!z1MnvzM*Kubs0h_r~{tAy&n ziPgLpAH4?M9t71O(d*spq_s-^FOrP^@`HM_r6{-GlUClAOzbK%_v~U}M3n5rU-9V@jk~O-)VS&QAEQxGn9fRaf;xa`r%) ziH!+|A_7t*C5%OG3(n-S&SApH z7PFmtV1$6O!S06$9B^f66-QxCenEkD;Q@IlGY%c1!fZ}HqobvL`s4g<TNnUgEmvv(D1){RVmHrIa^;&nq1Y4AEIbjTHu3^+TCuR>R6$<6$yYD%w>6Z zw-E>wRd~5T_~u|FM<*WZJO_JF1rA1-q=1HohASwjP7D_K3`9}@NsvHHOpF3RI-mpV zkTbu4aH#aTu38oxI@ww{C{_fIvxDT0$VRI`SWH%v(gOlzm%n+VhF3*YzBU8!++29h z9>jaa1MeS(I5;{o0^VPIB-;+1c~l>`f4(iG#}gA9tB4rPFC`@f);tXX&#YICekQE7 z9%UV+64>{{tdjWngV1LlakBLo&ZtqawjT1T(!Yy9l#aq9A)yQCUB&y)?B}(yvrs!X zLNkUEm?y7xth=2mfV_>Sux15LDG(o7!ZF9k4;5^&6Dx$@&X?hLaB@&$dcI}-?fv~cYn7-{^yLK=D6F%i$SQOq~-Pl_Q7P^p3Mk+ zi`6=(z4Bvo95!t*Ml%SZW5}yludGAT=jT09-L}PjdU(ez)rafk0>OUMczAg1wrLSc zGT*78rxy*`jTzh@kD#C$4Yz`k(S4jZ?+5&t5o}XrC6EezT?2!VeV;r}%zgX~fsMa9 zKNAQ$QK^>oaGrmeFo(}wBt=Ay?w=dZTISPMda0DprT=v#m?f*fmN2sd1X#y<0ofYS zm=PtkbEgIZfRL-4?oVFx3=ckzV+Fg=hp1MLh8r$X5f?|C=U1Qe7Hj?lFds|%*_R|l#aq@x_Y{KhCDp8sfK_bg-~@S5`zaE%fc5GZ78Zs? zJz)B0rggIb)j=%qih;I(zAo2KEEH#24WkY_HC5I1)6>&L+{7a-Z9wBVx0Z0mtfJBC zkKcXC8G#AST}>f}@yjJO(s29D_TM%pvNrgeAifrkXSPD+%6$MFz%Ri|^Nk=)Jp1mi zeE#iVk)7Hsri44CY(e6+&Fa!fPOD!|P7YBVXl6t`Dk?Jc{P}jD)Ckn>%Y58G4{RO5 z^liY9Bm%8`kN*loC=nKJyB#=)07Y5&2e=#jMxIbr>+eNJ?|@RG5`jlpaE>$iCRq=H zC!aML_$>g*TIPc@jFTUDfsa_`V^P_NA+=Cu1RD{zn$ia*FJhWdX8v;AsE`Q_&QZWO znGtHWA2_CC`Q_D(^5$mkCt3n39+(i~-a^A|Y4)&%(a_T~!`-5M>{tgeFPIb-N+l9| z5vv2PqcM~%-5z5yw(QSsT~S(a2%z3z=n;o74!4eL^WlF*mKK zhyz^kHl)uCaQQ0p@rQiZv9S2x+h@v!bQnwwb= zjVwJHx&8daRm|pDaO|X{8NnRkWJ!RErMm4%QlklSHIhU_I?-^=@mfGz9h%hb|x6{HifpBnF0>t}8-a--rs9C{G=WzXPX&{nl5a8$}>gcJW& zelp>yje!Pij;1D~uwg!l3PIWsapOiQ=k18#c7Y78<+_8~k7HU|T1np{xic850vOmS z6p@nI#0Rk$8G_fe8eiIsA(JJ@AJOvMvo9$lbt!NtW~e`C*EQ-GP0z`JKfQd!L$L$F zWRC4u2V0~*>){1KzP0=c;h?~Ya_`%h_vdCN1;0h5=6kE>XA28I(FfVmfL$UYin33i zxJcPvZgHQ;tG$DCaMF72?ovodePC5MPdrkGL}N1&$8EoCCW?Q{(IkZJ=4)AxinLI! z1Dv3k-M**gJq>&N=8dm9{|D)N+Wj{7u-sl@?O?wsLSTi`xEm-5J85)r<@v95je|-s zMQ`fr{0=9T!B{jtP+@{hstU(b{mib97gVgTM=9Ij^urc3Z29nQi7Xy0OtDO(Ie{)zjf`5vgXfEUbwJ0 zF5nxibM(iga`e;A3vTSv(Js}_?&`7+sMLRvoxO)1#lHx200rNi&Q+kaC->XD-+;em zqGU%wQLz`*{iGI<(`!<5a)!M`(QrE-cBgbL zj=})q(P~)b>~91ahvncR>kdv}tFh5V+rqRySCxcgCg?N+pkcX#hw~+*?nFn57v>M@ zAA9l)8#xgixg|5bNom&z#)#lbQ14!kh`56M26Qq(6$V<;*hw8ySbxq)II^aSmKIh7 znbo*VIxyb-ut^ZeQc-FtzLJI=5DXG(5}*{y51m_p&e5UPga6g*d8`p`F|iYmi~Cq3 z3+ISqioynm&*2650*57skY?+5!TKzIFH_}n@}$HE2zABBkDFiwnsYy;h+yaU0swVL z-HNTsiDgX<86Jj@8>u5~KgDNu)(t;eDQy$@O?#qLvbaMNE}*swaz zySiRA6mFe+_6mkzdH(s7n|XQreD0olPS!2BXBvQ0C|gst+zi`G3c(5r3TB718nVZ% z;B)|~2m=Y6+g(IR|78@^@w8Omt;h*e+^C|p z-`TCVe5J}9!dv7YAwZ(;@a)Xmz{q$RLk(l1+};p(0E%#LF+`yKU%r^U^r0f)jrZKh zKCB6YZPhh3t01M2ird@cVvK~h4>`Q3L@qauVL{e^-Edw|mP>m|5uo(+^Gd9(L=@p{ zf#o5Jn?IgY%_k{75qjp zkwG#0Qr*yS^u3*p4IKo=Bkn$}xvx>zPPlY}g_6>rC?UNo_!3;v81#7_ z9^Zi~r0ocfr%F-=?GV?lhf_8z^*+^sbZwjAsZ&)T%|a0uBe!(#5IOaN50|dQvDRBh zpw403X^?wMz4g-kEMx(M6FOWYz5;TMhuu4d#Kgq>lai7)9`G)D;01XW;<6y9&DdFN zf`WsCo!jBv6irOxk^0;&+T#sBg%~c|>L#Bz3i!)gTTfzoH{!Fh3zH|P4vtCR1@^1x z@??-I=8x}!2vw=6thC_fUAgA$0N5wtdupcf9TWlleyM6S{5 z{4k;Mwx4rG)_AqvL6UhcV*CD+soK}p8{FHPC2H}`CBfsA|)O@ob zq%8VbFAbFq6FCZ{yuSWO-F=z460@Ce(U72$j2lVViJikfGW2LI5rigUJzqGUzKzE3Q+j#sPaof8H?)#vliK_hi z^=mNpR3IR9f<2MfXhe*I3Y#6jw?NxAm>=LOAtp?Tf3Neb#g4?Wm7CAcFDaCdDz1eU+;xAWR4$_W?dY^YC~wll_p*J@r5V zqcw7Cv$564w8D)RJ9`L}L%q>T3NL zfbCWjeG0UFNk2Jw9OgDFVhR=U{7{bUK2x``xZ&|eHX9YVopCr?Gpw3??*%CU1mq!# zX8S@SEcpe_?qNFzq=m4}@Y>|K8$?@bMv|uLNH2HnD8qtJ_n5K!(%+u|wbz~f7e&FT zKW9qO6396BQIv=W5%B{reu>q50985NNK2uKIzS5jiNF$NrODz^m<%le7Cfmh++HuD z5~#4TW>*jz8(U(a@@KmXn8(NI;NgkH54GP2)n4L6_o?pt)Vb`6hXK}Ztxe{iOBs~h$OaH7CxpQDGU>QSx93j{U-U%%BCgD{H*RZdds$Vr5+JL*l~r=% zH$3Hqf1dKp+q?dVU;3c2A>2P;q5JMbJD>*8WWR}ckg)dDNO~q+Egq4A$5QNzJ<0noat+@M1t#_3JkH#l-p0H>T**8!K=$7Wrrde zPzc$2eMlrks1D8}w{OqIexM_&&6pUWpjgTCk0@J8=6|U9*53x{%}EMc2QD|`#2w^@ zbRhENwX|+^e|tuxZban^a0pP75)^E(fsIhLViJ3SFjIVFJf`EpE&)|~{=uH*ee^u@H2M$Z;8gWrMQ}fQkfj=?@Twf|bUPYW$}% zMz8^k1@ba7GE@hD3~!{kd3dNr#m2-?I!hEY5HT|B(z_DufUjiaCpj}kr=OxQnU zUb7IvWT428L{@X1kyo#-0v>>(kI6rzqM1Sf$S+^MSYP{w=R^D+I}KU-$nU~hi%gIE zzBnBpAj^gJsm+i17uO_;cQEHZJv}DGV7UQ|fIqu}B$4tG4$0xq{6#|`2<;=}{4DtI zK4=QGtI63vjg5`@p1(35cmcnnKsZFbF~$`^_AHqjmEL3D&9TT8*TqOOA?4pFer_Y2V8E?g%t%*wL`By_qFDh!7Y&6xAED$i z73+yO1+WEC(g^8i(}3Wna`14GgI6Xc-Xu;VW#aHi<_K)zm-(=@&BrClO`bh_=Jum@ z8xco5c=)gvg>DDoRW$!|KxER1DMc|@FUs`gOiZ>wSME>bKu{*2Y=Yv0pL_BD#9XtA z7=_|-6Omn$soCu|ZZh(KEj&D2!gF?4^);?*|0oXKhLRs211O9_Z|EmuTCfks0jis! z?h*$Ztt3HZUbj2lf%$P?aS_z%K-j9Qr}B5-&YQI+Ha0158S)o8+pwL9Edpke2=jpC zr3|MFT`;q8BKvj5LQczl$KpXUAPhGxQF8yksY9rM|657T|G+T*zkh7t?{dK=xwSi; S8pbL3M@8|7!ZSIOEB^=bDkZ%D literal 0 HcmV?d00001 diff --git a/bench/users-lat.png b/bench/users-lat.png new file mode 100644 index 0000000000000000000000000000000000000000..c09fedd6bb8a1aa3189fec37ee2e2b7722f30480 GIT binary patch literal 29934 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 literal 0 HcmV?d00001 diff --git a/bench/users-tps.png b/bench/users-tps.png new file mode 100644 index 0000000000000000000000000000000000000000..42a7526165cc659aa99498595176d7e7f41014ac GIT binary patch literal 30700 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 d?4Gw{+^$)Qof20YT5 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 MH`Dg}$sXJO4V4epy#N3J literal 0 HcmV?d00001 From e3a31c1d4fd4b704827a6ab82e51d435c71ade5c Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Tue, 4 Aug 2026 14:53:31 +0300 Subject: [PATCH 37/52] Make the checkpointer yield to a loaded cleaner pool (Stage 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two remaining in-run throughput dips sit exactly in the checkpoint write windows, where the checkpointer's ring traffic rides on top of the cleaner pool's peaks. Teach CheckpointWriteDelay a second, pressure-gated way to reach its nap: when the base schedule check says behind, but the pool's bin queue is at least half full and the lag is still within a bounded margin — Min(0.05, half the slack checkpoint_completion_target leaves), recomputed at every check since the target is SIGHUP-reloadable — the checkpointer naps anyway, so its writes land in the quieter phases of the window. Once the margin is used up, pacing is the stock behavior regardless of pressure, and immediate/shutdown checkpoints bypass the branch as before. IsCheckpointOnSchedule gains a slack parameter applied after the completion-target scaling, so the granted lag is exactly the margin. The checkpointer never touches the queue lock: the queue keeps an atomic depth mirror updated at the two nqueued mutation sites, and the hot check reads only that. A pressure_naps counter (exposed through test_dwb's cleaner counters) counts only margin-granted naps. The new TAP scenario parks the pool at an injection point, fills the queue past the hot threshold, and lets a timed — non-immediate — checkpoint pace over a dirty set sized well past the pacing slot count (at one page per nap a small set never falls behind schedule at an evaluation point and the branch never fires): pressure naps must register, the checkpoint must complete under sustained pressure, and a control checkpoint without pressure must add none. Also add the missing 019/020 entries to the module's meson TAP list. --- doc/src/sgml/config.sgml | 7 +- src/backend/postmaster/checkpointer.c | 44 ++++- src/backend/storage/dwb/dwb_cleaner.c | 31 ++++ src/include/storage/dwb.h | 6 + src/test/modules/test_dwb/meson.build | 2 + src/test/modules/test_dwb/t/020_ckpt_yield.pl | 160 ++++++++++++++++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 2 +- src/test/modules/test_dwb/test_dwb.c | 6 +- 8 files changed, 248 insertions(+), 10 deletions(-) create mode 100644 src/test/modules/test_dwb/t/020_ckpt_yield.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 870296ea318d9..8959869bd1697 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3573,7 +3573,12 @@ include_dir 'conf.d' buffers instead. The background writer then writes no data pages itself: when the pool falls momentarily behind, the scan pauses instead of writing and resumes as soon as a worker frees queue - space. The workers consume + space. The checkpointer also defers to a loaded pool: while the + pool's queue is hot, checkpoint writes pause within a small, + bounded slice of the + slack, so + checkpoint traffic lands in the quieter moments of the write + window. The workers consume slots. Setting it to 0 (the default) disables the pool and the background writer flushes its bins itself. diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 3914ac8da2622..d093623bc2d7b 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -52,6 +52,7 @@ #include "storage/aio_subsys.h" #include "storage/bufmgr.h" #include "storage/condition_variable.h" +#include "storage/dwb.h" #include "storage/fd.h" #include "storage/ipc.h" #include "storage/lwlock.h" @@ -163,7 +164,7 @@ static pg_time_t last_xlog_switch_time; static void ProcessCheckpointerInterrupts(void); static void CheckArchiveTimeout(void); -static bool IsCheckpointOnSchedule(double progress); +static bool IsCheckpointOnSchedule(double progress, double slack); static bool ImmediateCheckpointRequested(void); static bool CompactCheckpointerRequestQueue(void); static void UpdateSharedMemoryConfig(void); @@ -772,6 +773,7 @@ void CheckpointWriteDelay(int flags, double progress) { static int absorb_counter = WRITES_PER_ABSORB; + bool nap; /* Do nothing if checkpoint is being executed by non-checkpointer process */ if (!AmCheckpointerProcess()) @@ -780,12 +782,38 @@ CheckpointWriteDelay(int flags, double progress) /* * Perform the usual duties and take a nap, unless we're behind schedule, * in which case we just try to catch up as quickly as possible. + * + * Under double_writes an active cleaner pool competes with us for the + * ring and the array; while its bin queue is hot we keep napping a little + * past the schedule, spending a bounded slice of the completion-target + * slack so our writes land in the quieter phases of the window. The + * margin is recomputed from the live target on every check (it is + * SIGHUP-reloadable mid checkpoint) and caps the extra schedule lag; once + * it is used up, pacing is the stock behavior no matter the pressure. */ + nap = false; if (!(flags & CHECKPOINT_IMMEDIATE) && !ShutdownXLOGPending && !ShutdownRequestPending && - !ImmediateCheckpointRequested() && - IsCheckpointOnSchedule(progress)) + !ImmediateCheckpointRequested()) + { + if (IsCheckpointOnSchedule(progress, 0.0)) + nap = true; + else + { + double margin = Min(0.05, + (1.0 - CheckPointCompletionTarget) / 2.0); + + if (margin > 0.0 && DWBCleanerQueueHot() && + IsCheckpointOnSchedule(progress, margin)) + { + DWBCleanerCountPressureNap(); + nap = true; + } + } + } + + if (nap) { if (ConfigReloadPending) { @@ -839,7 +867,7 @@ CheckpointWriteDelay(int flags, double progress) * than the elapsed time/segments. */ static bool -IsCheckpointOnSchedule(double progress) +IsCheckpointOnSchedule(double progress, double slack) { XLogRecPtr recptr; struct timeval now; @@ -848,8 +876,12 @@ IsCheckpointOnSchedule(double progress) Assert(ckpt_active); - /* Scale progress according to checkpoint_completion_target. */ - progress *= CheckPointCompletionTarget; + /* + * Scale progress according to checkpoint_completion_target. The slack + * term is added after the scaling: it grants the caller that much extra + * elapsed fraction before the answer flips to "behind". + */ + progress = progress * CheckPointCompletionTarget + slack; /* * Check against the cached value first. Only do the more expensive diff --git a/src/backend/storage/dwb/dwb_cleaner.c b/src/backend/storage/dwb/dwb_cleaner.c index 66384d506871b..29a0c62ae8fcd 100644 --- a/src/backend/storage/dwb/dwb_cleaner.c +++ b/src/backend/storage/dwb/dwb_cleaner.c @@ -98,6 +98,8 @@ DWBCleanerShmemInit(void) pg_atomic_init_u64(&DWBCleanerQueue->pool_written_total, 0); pg_atomic_init_u64(&DWBCleanerQueue->skipped_pages, 0); pg_atomic_init_u64(&DWBCleanerQueue->deferred_bins, 0); + pg_atomic_init_u64(&DWBCleanerQueue->pressure_naps, 0); + pg_atomic_init_u32(&DWBCleanerQueue->depth, 0); ConditionVariableInit(&DWBCleanerQueue->cv_work); DWBCleanerQueue->capacity = DWB_CLEANER_QUEUE_CAPACITY; } @@ -138,6 +140,7 @@ DWBCleanerEnqueueBin(const int *buf_ids, int nbuf) bin->nbuf = nbuf; memcpy(bin->buf_ids, buf_ids, nbuf * sizeof(int)); ctl->nqueued++; + pg_atomic_write_u32(&ctl->depth, ctl->nqueued); LWLockRelease(DWBCleanerQueueLock); pg_atomic_fetch_add_u64(&ctl->enqueued_pages, nbuf); @@ -161,12 +164,40 @@ DWBCleanerDequeueBin(DWBCleanerBin *bin) *bin = ctl->bins[ctl->head]; ctl->head = (ctl->head + 1) % ctl->capacity; ctl->nqueued--; + pg_atomic_write_u32(&ctl->depth, ctl->nqueued); got = true; } LWLockRelease(DWBCleanerQueueLock); return got; } +/* + * Advisory pressure signal for the checkpointer: is the bin queue at + * least half full? Reads only the lock-free depth mirror — a stale + * answer merely shifts one 100ms pacing decision, so no lock is taken; + * nqueued itself stays under DWBCleanerQueueLock. + */ +bool +DWBCleanerQueueHot(void) +{ + DWBCleanerCtl *ctl = DWBCleanerQueue; + + if (ctl == NULL || !DWBCleanersActive()) + return false; + return pg_atomic_read_u32(&ctl->depth) >= ctl->capacity / 2; +} + +/* + * Count a checkpointer nap taken only because the queue was hot (the + * base schedule check alone would have kept writing). + */ +void +DWBCleanerCountPressureNap(void) +{ + if (DWBCleanerQueue != NULL) + pg_atomic_fetch_add_u64(&DWBCleanerQueue->pressure_naps, 1); +} + /* * The bgwriter folds the pool's completed writes into * PendingBgWriterStats.buf_written_clean once per round, keeping diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index c42792f10ba10..3f7f0cd1ed080 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -471,6 +471,10 @@ typedef struct DWBCleanerCtl * reclassification */ pg_atomic_uint64 deferred_bins; /* bins refused by a full queue and * carried over by the bgwriter */ + pg_atomic_uint64 pressure_naps; /* checkpointer naps taken only because + * the queue was hot */ + pg_atomic_uint32 depth; /* lock-free mirror of nqueued for the + * checkpointer's advisory pressure check */ ConditionVariable cv_work; /* one targeted signal per enqueued bin */ int capacity; /* head/nqueued and the bins are protected by DWBCleanerQueueLock */ @@ -528,6 +532,8 @@ extern bool DWBCleanersActive(void); extern bool DWBCleanerEnqueueBin(const int *buf_ids, int nbuf); extern uint64 DWBCleanerFetchPoolWritten(void); extern void DWBCleanerCountDeferral(void); +extern bool DWBCleanerQueueHot(void); +extern void DWBCleanerCountPressureNap(void); extern void DWBCleanerWorkersRegister(void); pg_noreturn extern void DWBCleanerWorkerMain(Datum main_arg); diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index 880bcc84db591..bd0e331a44e86 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -55,6 +55,8 @@ tests += { 't/016_bgwriter_bin.pl', 't/017_syncfs_retire.pl', 't/018_cleaners.pl', + 't/019_autovacuum_class.pl', + 't/020_ckpt_yield.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/020_ckpt_yield.pl b/src/test/modules/test_dwb/t/020_ckpt_yield.pl new file mode 100644 index 0000000000000..d8ce0dedebe72 --- /dev/null +++ b/src/test/modules/test_dwb/t/020_ckpt_yield.pl @@ -0,0 +1,160 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Pressure-aware checkpoint pacing: while the cleaner pool's bin queue +# is hot, the checkpointer keeps napping slightly past its schedule, +# inside a bounded slice of the completion-target slack. The scenario +# parks the pool at an injection point, fills the queue past the hot +# threshold, and lets a timed (non-immediate — SQL CHECKPOINT would set +# CHECKPOINT_IMMEDIATE and bypass the branch) checkpoint pace itself +# over a large dirty set: stock pacing oscillates around the schedule +# boundary, writing roughly one page per nap, so with the queue hot a +# stream of pressure naps must register — and the checkpoint must still +# complete while the pressure persists, proving the yield budget is +# bounded rather than a stall. A second timed checkpoint with the pool +# released and the queue drained must add no pressure naps. +# +# The file needs real timed-checkpoint cycles (checkpoint_timeout has a +# 30 s floor), so it runs for a bit over a minute by construction. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if (!defined $ENV{enable_injection_points} + || $ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'injection points not supported by this build'; +} + +my $node = PostgreSQL::Test::Cluster->new('dwb_ckpt_yield'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 1 +dwb_cleaner_workers = 2 +shared_buffers = 32MB +bgwriter_lru_maxpages = 0 +checkpoint_timeout = 30s +checkpoint_completion_target = 0.5 +log_checkpoints = on +autovacuum = off +)); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points'); + +$node->poll_query_until('postgres', + "SELECT count(*) = 2 FROM pg_stat_activity WHERE backend_type = 'dwb cleaner'" +) or die 'timed out waiting for the cleaner workers to start'; + +sub counter +{ + my ($name) = @_; + return $node->safe_psql('postgres', + "SELECT $name FROM test_dwb_cleaner_counters()"); +} + +sub ckpts_done +{ + return $node->safe_psql('postgres', + 'SELECT num_done FROM pg_stat_checkpointer'); +} + +# Park both workers before any dequeue: idle workers sleep on the queue +# condition variable and pass the loop top — where the point sits — +# only when woken, one targeted signal per enqueued claim. +$node->safe_psql('postgres', + q(CREATE TABLE t_park AS SELECT 1 AS id, repeat('p', 100) AS filler)); +$node->safe_psql('postgres', + "SELECT injection_points_attach('dwb-cleaner-loop', 'wait')"); +$node->safe_psql('postgres', "SELECT test_dwb_enqueue_block('t_park', 0)"); +$node->safe_psql('postgres', "SELECT test_dwb_enqueue_block('t_park', 0)"); +$node->poll_query_until( + 'postgres', q( + SELECT count(*) FILTER (WHERE wait_event = 'dwb-cleaner-loop') = 2 + FROM pg_stat_activity WHERE backend_type = 'dwb cleaner' +)) or die 'timed out waiting for both cleaners to park at the point'; +pass('both cleaners parked at the injection point'); + +# Fill the queue to capacity and keep it there: the scan is disabled +# and the pool is parked, so nothing drains it below the hot threshold +# (half of the 64-bin capacity) for the rest of the pressure phase. +$node->safe_psql( + 'postgres', q( + CREATE TABLE t_fill AS + SELECT g AS id, repeat('x', 800) AS filler + FROM generate_series(1, 800) g; +)); +for my $blk (0 .. 66) +{ + $node->psql('postgres', "SELECT test_dwb_enqueue_block('t_fill', $blk)"); +} +cmp_ok(counter('queued'), '>=', 32, 'the bin queue is hot'); + +# The dirty set the checkpoint paces over; it stays in shared buffers +# (no LRU scan, pool parked). It must be much larger than the pacing +# can absorb at one page per nap (completion window / nap quantum ≈ 150 +# slots), or the checkpointer never falls behind schedule at an +# evaluation point and the pressure branch is never reached — ~3000 +# pages against 150 slots keeps it behind for most of the window. +$node->safe_psql( + 'postgres', q( + CREATE TABLE t_dirt AS + SELECT g AS id, repeat('d', 800) AS filler + FROM generate_series(1, 24000) g; +)); + +my $naps_before = counter('pressure_naps'); +my $done_before = ckpts_done(); + +# The next timed checkpoint runs with the queue hot the whole way (the +# workers stay parked through the assertion). Completing under +# sustained pressure is itself the boundedness proof. +$node->poll_query_until('postgres', + "SELECT num_done > $done_before FROM pg_stat_checkpointer") + or die 'timed out waiting for the timed checkpoint under pressure'; +pass('the timed checkpoint completed under sustained queue pressure'); + +cmp_ok(counter('pressure_naps'), + '>', $naps_before, + 'the checkpointer took pressure naps while the queue was hot'); + +# Release the pool: detach BEFORE waking (a woken worker loops back to +# the point and would re-park with no wakeup left), then nudge until +# both are off the point — each wakeup releases one waiter. +$node->safe_psql('postgres', + "SELECT injection_points_detach('dwb-cleaner-loop')"); +my $deadline = time() + 30; +while (time() < $deadline) +{ + last + if $node->safe_psql('postgres', + "SELECT count(*) FROM pg_stat_activity WHERE wait_event = 'dwb-cleaner-loop'" + ) == 0; + $node->psql('postgres', + "SELECT injection_points_wakeup('dwb-cleaner-loop')"); +} +$node->poll_query_until('postgres', + 'SELECT queued = 0 FROM test_dwb_cleaner_counters()') + or die 'timed out waiting for the released pool to drain the queue'; +pass('the released pool drained the queue'); + +# Control: a comparable dirty set, the queue empty — the next timed +# (again non-immediate) checkpoint must add no pressure naps. +$node->safe_psql('postgres', + "UPDATE t_dirt SET filler = repeat('e', 800) WHERE id % 2 = 0"); +my $naps_quiet = counter('pressure_naps'); +my $done_quiet = ckpts_done(); +$node->poll_query_until('postgres', + "SELECT num_done > $done_quiet FROM pg_stat_checkpointer") + or die 'timed out waiting for the control timed checkpoint'; +is(counter('pressure_naps'), + $naps_quiet, 'no pressure naps without queue pressure'); + +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index bd729a8f97fe9..ceeedb6c46531 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -107,7 +107,7 @@ CREATE FUNCTION test_dwb_set_control_min_version(min_version int) CREATE FUNCTION test_dwb_cleaner_counters( OUT enqueued bigint, OUT written bigint, OUT skipped bigint, - OUT deferred bigint, OUT queued int) + OUT deferred bigint, OUT queued int, OUT pressure_naps bigint) RETURNS record STRICT AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index 854f8b6a59834..5efb3a765b50e 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -1112,8 +1112,8 @@ Datum test_dwb_cleaner_counters(PG_FUNCTION_ARGS) { TupleDesc tupdesc; - Datum values[5]; - bool nulls[5] = {0}; + Datum values[6]; + bool nulls[6] = {0}; int queued; check_cleaners_enabled(); @@ -1134,6 +1134,8 @@ test_dwb_cleaner_counters(PG_FUNCTION_ARGS) values[3] = Int64GetDatum( (int64) pg_atomic_read_u64(&DWBCleanerQueue->deferred_bins)); values[4] = Int32GetDatum(queued); + values[5] = Int64GetDatum( + (int64) pg_atomic_read_u64(&DWBCleanerQueue->pressure_naps)); PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); } From dda57a9f0a35094ac386f61c99c3bf7d48abb5cb Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Tue, 4 Aug 2026 15:50:28 +0300 Subject: [PATCH 38/52] Refresh the time-series benchmark charts The 2700-connection time series now shows the double-write build with the checkpointer-yield change: the checkpoint-window dips are gone from the DWB curve (worst 10 s sample 102.8K vs 98.6K before). --- bench/time-lat.png | Bin 58088 -> 56472 bytes bench/time-tps.png | Bin 56726 -> 55703 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/bench/time-lat.png b/bench/time-lat.png index c199714017843c0b429a8c94ced9f152cf29637a..ad98779bc5b95a23984737cb2eb71ea85842dc9d 100644 GIT binary patch literal 56472 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

7@^qD0F70usvz{Bg#fxC)0BSV- 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

yf z!h{qKLLg=q1_ILcsAphs>|lLGD8^MqX!}ZqycG02SQCe*GZd3CT*S$pjq%gv+!^kFP zPX2jm`pOn(;*gm6@nNe1Gt%3~$eq2A` za`*1tc*TIt-FEi~v1lvAA#~ z2~t`?nqh^g&V;Jf8gc2WTc_XX;@<)EEQPURY5j2B3P-MK3xJKx*AOLm0!FX zeI_=BaP<>cJfvw8D^8RqWLQv=xVpQqgVAs4CsB{zMkhBxq@#ZhS#)b2sW`1itm0>Z zTt7m#{|E%chFryp7s86QX8NPZ&-v;qy_S!gr=q>4hV?LJoA65QdrBmTNhDx#)UxP( zE(aSUBjxU)Opk7KolxKjeBlUYkKi%LbNe8OL3(BcY%jVWTvhOxAE9wa&z}rpMI-yy z+}Q>>0XtgML1a%klu3+Y&yy9o5pPVtz1hj=0g9D4*((pg zSX^gOrNuhdRJ8hYm9>iN9Y`fp=kxv?7@H zJqs_d?~4D)q^CS0X<(?=VV8q#YicFn3`UPc`@O8OysV*=lOQ+^#R=a%6@3?*x7f9C zqROp>brdrI-jcmePC5Kye0)@3WCOv?fhHtPK-!5?tANf;-GD33!@iD>K|`OzK7ZIB z4eh`X5eYWHin|CL0;7Gw(c0y(vg>F=E+@@e8#aU?i>|;6z3|VNfFde)a%qpfJWEBQ zoAmZ_5djOLN9_K(5;rIimzm_-45yDLDL~$7;fl3Wi!lxIdA6HF|}Pq4>oya zUtVW2llBnEKFGSJLmtKm)XcC~uY%zkl`}})l#&v5okXc~&>M(QO4)UGcDrej19d>a zANOcB^e-Z<5DqM6hRC%PID>djaPK%4I&caxaTx{JwY?Bd`I^2yZqnfR78NwRXBB4E z09hNi19GGtJ2y*7ttSo^;D9yg0L9v4Mf^yNM{NmttPTPV)_#6b5%{Q0Ac?ME_eBWl z6w)#cB3W480(B|yWKq`9RaTb~$QOVCjY zqt~Gi3+k{4LJcHtslmqt^`cMnG2DUzIn-(ZqB zyyQM)!4Gqbz#m79V)#37OOYT5`m^(@l(7O|VryAS4G0MI-A2P(MK-J?upAfUm{lH-KBKM?+aC(#~?Bq@zpZ#y~6t3DSXz(oOA^IQomB1jWbi|8oN_U4Z46Q)`6$T*knY2En-xnj{$a z{s~?XlVYa?BPYX_rRkEu7%i%~v=sni&FcR+r2iM|^ZypJiSGCRW~BZft~M3mzd^=z U-Q=fs3jU{|dO+pA^3n7EA3)j8CjbBd literal 56726 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 S8pbL3M@8|7!ZSIOEB^=bDkZ%D From 5885066f341de4fd1514a02c6fc243f87801add1 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Wed, 5 Aug 2026 08:17:47 +0300 Subject: [PATCH 39/52] Add the replication benchmark charts Assets for the primary/synchronous-standby comparison: throughput and shipped WAL per client count, and the standby replay backlog over the run. --- bench/README.md | 4 ++++ bench/repl-lag.png | Bin 0 -> 42327 bytes bench/repl-tps-wal.png | Bin 0 -> 22085 bytes 3 files changed, 4 insertions(+) create mode 100644 bench/repl-lag.png create mode 100644 bench/repl-tps-wal.png diff --git a/bench/README.md b/bench/README.md index 848f219c00b69..cf739c44eb70e 100644 --- a/bench/README.md +++ b/bench/README.md @@ -10,3 +10,7 @@ and data checksums enabled, converted from the same reference cluster. connection count at `checkpoint_timeout = 300s`. * `time-tps.png`, `time-lat.png` — TPS and average latency over the 900 s run at 2700 connections (10 s pgbench samples, 30 s step). +* `repl-tps-wal.png`, `repl-lag.png` — the primary/synchronous-standby + series: throughput and shipped WAL per client count, and the standby's + replay backlog over the run. Both instances share the stand, split by + socket, with a netem-emulated 10 GbE hop between them. diff --git a/bench/repl-lag.png b/bench/repl-lag.png new file mode 100644 index 0000000000000000000000000000000000000000..cf0fc251649944df4eefdf037d2bf53bfe7cccf9 GIT binary patch literal 42327 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

D;D7@Q0d}yC!*2&Xxi@c{?oye)``By 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 literal 0 HcmV?d00001 diff --git a/bench/repl-tps-wal.png b/bench/repl-tps-wal.png new file mode 100644 index 0000000000000000000000000000000000000000..3ee8c9aec8078423254685288815ec4bc2e07964 GIT binary patch literal 22085 zcmeIac{tT;-#5I}u67c;L38YiG7ptRiQS|{WhQf!d6pq_?RF{^wXjMFh0OC9p%P({ znamAjCUfTJbK2K^UH5&z?{nPG`yR*hy!Rh($I+p%*7^q${uA%A+V+g*6$*vTfc#~OmX0!` zP}EYWC;w2r8a&wMqN7&7us9;_|M@-7@4vqM^CHd1%qn=20t_Ph@9TjjUP zBq^_PVZh^~WWW1IJ-3~!3Po6$a4?j!-%OpzSBhf&!^h-1*GkWnlmEZD%!La;pHtcv=0;HJi&kIwpgYp0;Il zXYuVgAh-F>*4T)wc31i5&!0EW-@SXcwu|wIeZz+E*RPMJoybpIvssAe(4kD7Ninxc zOR2dZg+}>nuf=xvRELxI3z2e*r~7e8X&D$8$Wf^&TJ4eYoAV|bwAC4V≺Ts!|+m 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 literal 0 HcmV?d00001 From 21cd9088b1bcfdf740d20094a493314c91cdd843 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Wed, 5 Aug 2026 13:46:19 +0300 Subject: [PATCH 40/52] Read pages for replay in a pool of background workers A standby of a cluster running io_torn_pages_protection = double_writes has no full-page images to apply, so replay must fetch every page it modifies. Doing that from the startup process costs about half of its single core: kernel advice per block, the read itself, the checksum, and the search for a victim buffer. On the primary-to-sync-standby benchmark that showed up as replay falling behind without bound while the primary kept up. Add a pool of background workers that read those pages into shared buffers ahead of replay. Where the prefetcher would issue advice, it now publishes the block to a ring in shared memory; a worker reads it and records which buffer it landed in, and replay picks that up through the recent-buffer hint XLogReadBufferExtended() already validates. The redo path itself is unchanged, and nothing here is an obligation: an unanswered request, a failed read or an evicted buffer all mean replay reads the page itself, exactly as with the pool disabled. The pool is off by default and sized by replay_warm_workers, with replay_warm_queue_size for the ring. The workers hold no database connection, so they can serve crash recovery from the first record; having no pg_stat_activity row, they advertise their pids in shared memory. Reading pages ahead of replay means reading them next to relations replay is dropping and truncating, and DropRelationBuffers() requires that nobody else be loading pages of the relation while it works. A worker holds ReplayWarmReadLock shared across its checks and its read; smgrdounlinkall(), smgrtruncate(), dropdb() and dbase_redo() hold it exclusively across the buffer drop and the file operation that follows, so a worker either finishes before the drop scans the pool or starts after the relation is gone. The exclusive side also bumps an epoch, and a worker seeing a new one releases its own smgr state: without a database connection it receives no cache invalidations and would otherwise trust a size from before the truncation. --- doc/src/sgml/config.sgml | 52 ++ src/backend/access/transam/Makefile | 3 +- src/backend/access/transam/meson.build | 1 + src/backend/access/transam/xlogprefetcher.c | 106 ++- src/backend/access/transam/xlogreader.c | 3 + src/backend/access/transam/xlogutils.c | 23 +- src/backend/access/transam/xlogwarm.c | 807 ++++++++++++++++++ src/backend/commands/dbcommands.c | 20 + src/backend/postmaster/bgworker.c | 4 + src/backend/postmaster/postmaster.c | 8 + src/backend/storage/buffer/bufmgr.c | 41 +- src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/smgr/smgr.c | 19 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/misc/guc_tables.c | 25 + src/backend/utils/misc/postgresql.conf.sample | 4 + src/include/access/xlogreader.h | 20 + src/include/access/xlogwarm.h | 62 ++ src/include/storage/bufmgr.h | 3 + src/include/storage/lwlocklist.h | 1 + src/test/modules/test_dwb/meson.build | 1 + .../modules/test_dwb/t/021_replay_warm.pl | 466 ++++++++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 22 + src/test/modules/test_dwb/test_dwb.c | 124 +++ 24 files changed, 1801 insertions(+), 19 deletions(-) create mode 100644 src/backend/access/transam/xlogwarm.c create mode 100644 src/include/access/xlogwarm.h create mode 100644 src/test/modules/test_dwb/t/021_replay_warm.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 8959869bd1697..a7780f7002337 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -4361,6 +4361,58 @@ include_dir 'conf.d' + + replay_warm_workers (integer) + + replay_warm_workers configuration parameter + + + + + Number of background workers that read the blocks recovery is about to + modify into shared buffers, ahead of replay. The default is 0, which + disables the pool; recovery then relies on + advising the operating system + and reads the blocks in the startup process itself. + + + This matters most when the WAL stream carries no full-page images, + which is the case on a standby of a cluster running + set to + double_writes: every replayed record then needs its + page fetched, and doing that from the single replay process can make + it the limit on how fast the standby keeps up. With the pool enabled + that fetching, including its checksum verification and its buffer + allocation, happens in the workers instead. + + + The workers take slots from + , take no database + connection, and start with the postmaster, so they also serve crash + recovery. Their requests are advisory throughout: anything they do + not get to in time is simply read by the replay process, as it would + be with the pool disabled. This parameter can only be set at server + start. + + + + + + replay_warm_queue_size (integer) + + replay_warm_queue_size configuration parameter + + + + + How many block requests the pool enabled by + can hold at once, which also + bounds how far ahead of replay the server looks for blocks to warm. + The default is 256. This parameter can only be set at server start. + + + + wal_decode_buffer_size (integer) diff --git a/src/backend/access/transam/Makefile b/src/backend/access/transam/Makefile index 661c55a9db789..5daedb962e4b3 100644 --- a/src/backend/access/transam/Makefile +++ b/src/backend/access/transam/Makefile @@ -36,7 +36,8 @@ OBJS = \ xlogreader.o \ xlogrecovery.o \ xlogstats.o \ - xlogutils.o + xlogutils.o \ + xlogwarm.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/access/transam/meson.build b/src/backend/access/transam/meson.build index e8ae9b13c8e49..019992993ee61 100644 --- a/src/backend/access/transam/meson.build +++ b/src/backend/access/transam/meson.build @@ -24,6 +24,7 @@ backend_sources += files( 'xlogrecovery.c', 'xlogstats.c', 'xlogutils.c', + 'xlogwarm.c', ) # used by frontend programs to build a frontend xlogreader diff --git a/src/backend/access/transam/xlogprefetcher.c b/src/backend/access/transam/xlogprefetcher.c index 7735562db01d1..31e16eeec053a 100644 --- a/src/backend/access/transam/xlogprefetcher.c +++ b/src/backend/access/transam/xlogprefetcher.c @@ -29,6 +29,7 @@ #include "access/xlogprefetcher.h" #include "access/xlogreader.h" +#include "access/xlogwarm.h" #include "catalog/pg_control.h" #include "catalog/storage_xlog.h" #include "commands/dbcommands_xlog.h" @@ -75,6 +76,16 @@ int recovery_prefetch = RECOVERY_PREFETCH_TRY; #define RecoveryPrefetchEnabled() false #endif +/* + * The lookahead machinery — decoding ahead, the relation filters, the + * distance logic — serves two consumers now, and runs if either wants it. + * The warm pool wants it whenever it is configured: it does not issue + * kernel advice, so neither USE_PREFETCH nor maintenance_io_concurrency has + * any say over it. + */ +#define RecoveryLookaheadEnabled() \ + (RecoveryPrefetchEnabled() || XLogWarmPoolActive()) + static int XLogPrefetchReconfigureCount = 0; /* @@ -286,7 +297,7 @@ lrq_complete_lsn(LsnReadQueue *lrq, XLogRecPtr lsn) if (lrq->tail == lrq->size) lrq->tail = 0; } - if (RecoveryPrefetchEnabled()) + if (RecoveryLookaheadEnabled()) lrq_prefetch(lrq); } @@ -389,6 +400,9 @@ XLogPrefetcherAllocate(XLogReaderState *reader) void XLogPrefetcherFree(XLogPrefetcher *prefetcher) { + /* the decoded records go away with the reader, so must their requests */ + XLogWarmCancelAll(); + lrq_free(prefetcher->streaming_read); hash_destroy(prefetcher->filter_table); pfree(prefetcher); @@ -503,11 +517,11 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn) } /* - * If prefetching is disabled, we don't need to analyze the record - * or issue any prefetches. We just need to cause one record to - * be decoded. + * If neither the advice nor the warm pool wants blocks, we don't + * need to analyze the record or issue any prefetches. We just + * need to cause one record to be decoded. */ - if (!RecoveryPrefetchEnabled()) + if (!RecoveryLookaheadEnabled()) { *lsn = InvalidXLogRecPtr; return LRQ_NEXT_NO_IO; @@ -763,6 +777,43 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn) return LRQ_NEXT_NO_IO; } + /* + * With the warm pool running, a miss is handed to a worker + * instead of being turned into kernel advice: the worker reads + * the page into a shared buffer, which is what replay actually + * needs, and none of that work lands on this process. + */ + if (XLogWarmPoolActive()) + { + Buffer resident; + uint64 request_id; + int slot_no; + + resident = LookupSharedBuffer(reln, block->forknum, + block->blkno); + if (BufferIsValid(resident)) + { + /* Cache hit, nothing to do. */ + XLogPrefetchIncrement(&SharedStats->hit); + block->prefetch_buffer = resident; + return LRQ_NEXT_NO_IO; + } + + slot_no = XLogWarmPublish(block->rlocator, block->forknum, + block->blkno, &request_id); + if (slot_no == XLOGWARM_NO_SLOT) + { + /* pool behind: replay will read this block itself */ + return LRQ_NEXT_NO_IO; + } + + block->warm_slot = slot_no; + block->warm_request = request_id; + XLogPrefetchIncrement(&SharedStats->prefetch); + block->prefetch_buffer = InvalidBuffer; + return LRQ_NEXT_IO; + } + /* Try to initiate prefetching. */ result = PrefetchSharedBuffer(reln, block->forknum, block->blkno); if (BufferIsValid(result.recent_buffer)) @@ -961,6 +1012,13 @@ XLogPrefetcherIsFiltered(XLogPrefetcher *prefetcher, RelFileLocator rlocator, void XLogPrefetcherBeginRead(XLogPrefetcher *prefetcher, XLogRecPtr recPtr) { + /* + * This will forget about any in-flight IO, so the requests those decoded + * records referred to must be withdrawn: nobody will ever collect them, + * and slots nobody collects would eventually fill the ring. + */ + XLogWarmCancelAll(); + /* This will forget about any in-flight IO. */ prefetcher->reconfigure_count--; @@ -995,7 +1053,21 @@ XLogPrefetcherReadRecord(XLogPrefetcher *prefetcher, char **errmsg) if (prefetcher->streaming_read) lrq_free(prefetcher->streaming_read); - if (RecoveryPrefetchEnabled()) + if (XLogWarmPoolActive()) + { + /* + * The pool's ring is what bounds requests in flight, and it is + * sized at server start: maintenance_io_concurrency can be raised + * at runtime and must not be able to push the lookahead past the + * ring, which would only produce requests that get dropped for + * want of a slot. + */ + max_inflight = Max(replay_warm_queue_size / 2, 1); + max_distance = Min(max_inflight * XLOGPREFETCHER_DISTANCE_MULTIPLIER, + replay_warm_queue_size); + max_distance = Max(max_distance, max_inflight); + } + else if (RecoveryPrefetchEnabled()) { Assert(maintenance_io_concurrency > 0); max_inflight = maintenance_io_concurrency; @@ -1056,6 +1128,28 @@ XLogPrefetcherReadRecord(XLogPrefetcher *prefetcher, char **errmsg) */ Assert(record == prefetcher->reader->record); + /* + * Collect whatever the warm pool managed to read for this record. A + * buffer collected here is only a hint, exactly like the one the cache + * lookup above leaves behind, and XLogReadBufferExtended() validates it; + * an unanswered request just leaves replay to read the block itself. + */ + if (XLogWarmPoolActive()) + { + for (int block_id = 0; block_id <= record->max_block_id; block_id++) + { + DecodedBkpBlock *block = &record->blocks[block_id]; + + if (!block->in_use || block->warm_slot == XLOGWARM_NO_SLOT) + continue; + + block->prefetch_buffer = XLogWarmCollect(block->warm_slot, + block->warm_request); + block->warm_hint = BufferIsValid(block->prefetch_buffer); + block->warm_slot = XLOGWARM_NO_SLOT; + } + } + /* * If maintenance_io_concurrency is set very low, we might have started * prefetching some but not all of the blocks referenced in the record diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c index 29d4df7a996ef..d485dfdda2204 100644 --- a/src/backend/access/transam/xlogreader.c +++ b/src/backend/access/transam/xlogreader.c @@ -1786,6 +1786,9 @@ DecodeXLogRecord(XLogReaderState *state, blk->has_data = ((fork_flags & BKPBLOCK_HAS_DATA) != 0); blk->prefetch_buffer = InvalidBuffer; + blk->warm_slot = XLOGWARM_NO_SLOT; + blk->warm_request = 0; + blk->warm_hint = false; COPY_HEADER_FIELD(&blk->data_len, sizeof(uint16)); /* cross-check that the HAS_DATA flag is set iff data_length > 0 */ diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c index 0d67f256afedf..50cf22ad1c341 100644 --- a/src/backend/access/transam/xlogutils.c +++ b/src/backend/access/transam/xlogutils.c @@ -23,6 +23,7 @@ #include "access/xlogrecovery.h" #include "access/xlog_internal.h" #include "access/xlogutils.h" +#include "access/xlogwarm.h" #include "miscadmin.h" #include "storage/fd.h" #include "storage/smgr.h" @@ -432,6 +433,18 @@ XLogReadBufferForRedoExtended(XLogReaderState *record, else { *buf = XLogReadBufferExtended(rlocator, forknum, blkno, mode, prefetch_buffer); + + /* + * A hint the pool produced and replay could not use means the page + * was evicted between the worker reading it and replay reaching it — + * the pool running too far ahead of replay, which is worth knowing + * about. Hints the buffer lookup left behind say nothing about the + * pool, so only the pool's own answers are counted. + */ + if (XLogRecGetBlock(record, block_id)->warm_hint && + mode == RBM_NORMAL && *buf != prefetch_buffer) + XLogWarmCountStale(); + if (BufferIsValid(*buf)) { if (mode != RBM_ZERO_AND_LOCK && mode != RBM_ZERO_AND_CLEANUP_LOCK) @@ -493,11 +506,13 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum, /* Do we have a clue where the buffer might be already? */ if (BufferIsValid(recent_buffer) && - mode == RBM_NORMAL && - ReadRecentBuffer(rlocator, forknum, blkno, recent_buffer)) + mode == RBM_NORMAL) { - buffer = recent_buffer; - goto recent_buffer_fast_path; + if (ReadRecentBuffer(rlocator, forknum, blkno, recent_buffer)) + { + buffer = recent_buffer; + goto recent_buffer_fast_path; + } } /* Open the relation at smgr level */ diff --git a/src/backend/access/transam/xlogwarm.c b/src/backend/access/transam/xlogwarm.c new file mode 100644 index 0000000000000..3adeecfad70c4 --- /dev/null +++ b/src/backend/access/transam/xlogwarm.c @@ -0,0 +1,807 @@ +/*------------------------------------------------------------------------- + * + * xlogwarm.c + * Replay prefetch worker pool: reads the pages replay is about to need + * into shared buffers, so the startup process does not spend its single + * core fetching them. + * + * Without full-page images in the WAL stream — the standby of a cluster + * running io_torn_pages_protection = double_writes — replay has to fetch + * every page it modifies. Doing that from the startup process costs about + * half of its core: issuing kernel advice for each block, then reading the + * page with its copy out of the page cache, then verifying its checksum, + * then finding a victim buffer to put it in. All of that is work another + * process can do in parallel, ahead of replay. + * + * The prefetcher already decodes WAL ahead of replay and already filters + * the blocks that must not be touched. Where it would issue advice, it + * instead publishes the block here; a worker reads it into a shared buffer + * and records which buffer that was. Replay picks the answer up as a + * recent-buffer hint, which XLogReadBufferExtended() already knows how to + * validate, so the redo path itself is unchanged. + * + * Nothing here is an obligation. A slot that no worker got to, a read that + * failed, a buffer that was evicted before replay reached it — each simply + * means replay reads the page itself, exactly as it does with the pool + * disabled. Replay never sleeps on a slot. + * + * The queue is a ring of slots recycled in publication order. One + * publisher (the startup process) and several consumers coordinate through + * the slot state alone: + * + * FREE/DONE/FAILED --(publisher)--> PUBLISHED + * PUBLISHED --(worker)-----> CLAIMED + * CLAIMED --(worker)-----> DONE | FAILED + * + * Both worker transitions are compare-and-swap, so several consumers cannot + * claim one slot. A claimed slot belongs to its worker until that worker + * leaves it: nothing else ever writes it, which is what keeps the result and + * the request id it was produced for a consistent pair. The publisher + * ignores an answer whose id is not the one it published, so an answer that + * arrives after replay has moved on is simply not picked up. + * + * A worker that exits while holding a slot returns it on the way out, so the + * ring does not shrink when a worker is signalled or throws a FATAL error; a + * worker that dies in an uglier way takes the whole cluster through a restart + * cycle, which rebuilds this ring from scratch. The one case that does cost + * a slot for good is a worker wedged inside a read that never returns, and + * replay would be wedged on that page too. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/access/transam/xlogwarm.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/xlogwarm.h" +#include "miscadmin.h" +#include "pgstat.h" +#include "postmaster/bgworker.h" +#include "postmaster/interrupt.h" +#include "storage/bufmgr.h" +#include "storage/condition_variable.h" +#include "storage/ipc.h" +#include "storage/proc.h" +#include "storage/shmem.h" +#include "storage/smgr.h" +#include "tcop/tcopprot.h" +#include "utils/guc.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/resowner.h" +#include "utils/wait_event.h" + +/* GUCs */ +int replay_warm_workers = 0; +int replay_warm_queue_size = 256; + + +typedef enum XLogWarmState +{ + XLOGWARM_FREE = 0, + XLOGWARM_PUBLISHED, + XLOGWARM_CLAIMED, + XLOGWARM_DONE, + XLOGWARM_FAILED, +} XLogWarmState; + +typedef struct XLogWarmSlot +{ + pg_atomic_uint32 state; /* XLogWarmState */ + + /* payload, written by the publisher while it owns the slot */ + uint64 request_id; + RelFileLocator rlocator; + ForkNumber forknum; + BlockNumber blkno; + + /* result, written by the claiming worker */ + uint64 result_id; + Buffer result_buffer; +} XLogWarmSlot; + +typedef struct XLogWarmCtl +{ + ConditionVariable cv_work; + + /* publisher side */ + pg_atomic_uint64 published; + pg_atomic_uint64 dropped_full; + pg_atomic_uint64 collected; + pg_atomic_uint64 missed; + pg_atomic_uint64 stale; + pg_atomic_uint64 cancelled; + pg_atomic_uint64 released; + + /* worker side */ + pg_atomic_uint64 claimed; + pg_atomic_uint64 reads; + pg_atomic_uint64 hits; + pg_atomic_uint64 failed; + pg_atomic_uint64 vanished; + pg_atomic_uint64 discarded; + + int capacity; + pg_atomic_uint32 hand; /* where consumers start scanning */ + + /* + * Bumped under ReplayWarmReadLock whenever a relation or a database is + * about to lose its files. A worker has no database connection and so + * receives no cache invalidations: without this it could keep a + * relation's cached size — or its open segments — from before the + * file changed underneath it. Read under the lock held shared. + */ + uint64 drop_epoch; + + /* + * Worker pids, published by the workers themselves. Without a database + * connection there is no pg_stat_activity row to see them in, and a pool + * nobody can see is a pool nobody can diagnose. + */ + pg_atomic_uint32 worker_pids[XLOGWARM_MAX_WORKERS]; + + XLogWarmSlot slots[FLEXIBLE_ARRAY_MEMBER]; +} XLogWarmCtl; + +static XLogWarmCtl * XLogWarmQueue = NULL; + +static void XLogWarmWorkerExit(int code, Datum arg); + +/* publisher-private state */ +static uint64 next_request_id = 1; +static int publish_hand = 0; + +/* + * Worker-private: the drop epoch this worker's smgr state is good for. + */ +static uint64 my_drop_epoch = 0; + +/* + * Worker-private: the slot this worker holds, or -1. Read on the way out to + * hand the slot back, so a worker that is signalled away does not take a slot + * of the ring with it. + */ +static int my_claimed_slot = -1; + +Size +XLogWarmShmemSize(void) +{ + if (replay_warm_workers == 0) + return 0; + + return add_size(offsetof(XLogWarmCtl, slots), + mul_size(replay_warm_queue_size, sizeof(XLogWarmSlot))); +} + +void +XLogWarmShmemInit(void) +{ + bool found; + + if (replay_warm_workers == 0) + return; + + XLogWarmQueue = (XLogWarmCtl *) + ShmemInitStruct("Replay Warm Queue", XLogWarmShmemSize(), &found); + + if (!found) + { + memset(XLogWarmQueue, 0, XLogWarmShmemSize()); + ConditionVariableInit(&XLogWarmQueue->cv_work); + pg_atomic_init_u64(&XLogWarmQueue->published, 0); + pg_atomic_init_u64(&XLogWarmQueue->dropped_full, 0); + pg_atomic_init_u64(&XLogWarmQueue->collected, 0); + pg_atomic_init_u64(&XLogWarmQueue->missed, 0); + pg_atomic_init_u64(&XLogWarmQueue->stale, 0); + pg_atomic_init_u64(&XLogWarmQueue->cancelled, 0); + pg_atomic_init_u64(&XLogWarmQueue->released, 0); + pg_atomic_init_u64(&XLogWarmQueue->claimed, 0); + pg_atomic_init_u64(&XLogWarmQueue->reads, 0); + pg_atomic_init_u64(&XLogWarmQueue->hits, 0); + pg_atomic_init_u64(&XLogWarmQueue->failed, 0); + pg_atomic_init_u64(&XLogWarmQueue->vanished, 0); + pg_atomic_init_u64(&XLogWarmQueue->discarded, 0); + pg_atomic_init_u32(&XLogWarmQueue->hand, 0); + XLogWarmQueue->drop_epoch = 0; + XLogWarmQueue->capacity = replay_warm_queue_size; + + for (int i = 0; i < XLOGWARM_MAX_WORKERS; i++) + pg_atomic_init_u32(&XLogWarmQueue->worker_pids[i], 0); + + for (int i = 0; i < replay_warm_queue_size; i++) + pg_atomic_init_u32(&XLogWarmQueue->slots[i].state, XLOGWARM_FREE); + } +} + +/* + * True when blocks may be handed to the pool. The pool exists for the + * duration of the postmaster; it is idle whenever nothing publishes. + */ +bool +XLogWarmPoolActive(void) +{ + return XLogWarmQueue != NULL; +} + +/* + * Publish one block for a worker to read. + * + * Returns the slot the request went into and sets *request_id, or + * XLOGWARM_NO_SLOT when the ring has no reusable slot, in which case the + * caller simply leaves the block unwarmed. + */ +int +XLogWarmPublish(RelFileLocator rlocator, ForkNumber forknum, + BlockNumber blkno, uint64 *request_id) +{ + XLogWarmSlot *slot = NULL; + int capacity = XLogWarmQueue->capacity; + int slot_no = XLOGWARM_NO_SLOT; + + Assert(XLogWarmQueue != NULL); + + /* + * A slot still PUBLISHED or CLAIMED belongs to an earlier request that + * has not been dealt with yet, and taking it back is never right — a + * claimed slot is being written by its worker, and a second writer would + * tear the result apart. So the publisher writes only slots nobody + * holds, and looks past the ones somebody does: a single slow worker must + * not stop the ring, which it would if publication insisted on one slot. + */ + for (int i = 0; i < capacity; i++) + { + int candidate = (publish_hand + i) % capacity; + uint32 state = pg_atomic_read_u32(&XLogWarmQueue->slots[candidate].state); + + if (state != XLOGWARM_PUBLISHED && state != XLOGWARM_CLAIMED) + { + slot_no = candidate; + slot = &XLogWarmQueue->slots[candidate]; + break; + } + } + + /* + * Every slot is spoken for: the pool is behind, and this block goes + * unwarmed. That is the pool's back-pressure — replay reads the block + * itself and never waits for a worker. + */ + if (slot == NULL) + { + pg_atomic_fetch_add_u64(&XLogWarmQueue->dropped_full, 1); + return XLOGWARM_NO_SLOT; + } + + slot->request_id = next_request_id; + slot->rlocator = rlocator; + slot->forknum = forknum; + slot->blkno = blkno; + + /* the payload must be visible before a worker can see the state */ + pg_write_barrier(); + pg_atomic_write_u32(&slot->state, XLOGWARM_PUBLISHED); + + *request_id = next_request_id++; + publish_hand = (slot_no + 1) % XLogWarmQueue->capacity; + pg_atomic_fetch_add_u64(&XLogWarmQueue->published, 1); + + ConditionVariableSignal(&XLogWarmQueue->cv_work); + + return slot_no; +} + +/* + * Collect the buffer a worker read for this request, if it has one. + * + * Returns InvalidBuffer when the request was never claimed, is still being + * read, failed, or the slot has moved on to another request: in every one + * of those cases replay reads the page itself. The returned buffer is only + * a hint and is not pinned — the caller validates it, as it does for any + * recent-buffer hint. + */ +Buffer +XLogWarmCollect(int slot_no, uint64 request_id) +{ + XLogWarmSlot *slot; + Buffer buffer; + + Assert(XLogWarmQueue != NULL); + Assert(slot_no >= 0 && slot_no < XLogWarmQueue->capacity); + + slot = &XLogWarmQueue->slots[slot_no]; + + if (pg_atomic_read_u32(&slot->state) != XLOGWARM_DONE) + { + pg_atomic_fetch_add_u64(&XLogWarmQueue->missed, 1); + return InvalidBuffer; + } + + /* the state must be observed before the result it advertises */ + pg_read_barrier(); + + if (slot->result_id != request_id) + { + pg_atomic_fetch_add_u64(&XLogWarmQueue->missed, 1); + return InvalidBuffer; + } + + buffer = slot->result_buffer; + if (!BufferIsValid(buffer)) + { + pg_atomic_fetch_add_u64(&XLogWarmQueue->missed, 1); + return InvalidBuffer; + } + + pg_atomic_fetch_add_u64(&XLogWarmQueue->collected, 1); + return buffer; +} + +/* + * Withdraw every outstanding request. + * + * The prefetcher throws its decoded records away when the read position + * moves (XLogPrefetcherBeginRead) and at the end of recovery, taking the + * slot references with them. Requests nobody claimed are freed here; + * requests a worker is inside of are left alone — that worker will finish + * into DONE, and the slot becomes reusable on the next pass of the ring. + */ +void +XLogWarmCancelAll(void) +{ + if (XLogWarmQueue == NULL) + return; + + for (int i = 0; i < XLogWarmQueue->capacity; i++) + { + XLogWarmSlot *slot = &XLogWarmQueue->slots[i]; + uint32 expected = XLOGWARM_PUBLISHED; + + if (pg_atomic_compare_exchange_u32(&slot->state, &expected, + XLOGWARM_FREE)) + pg_atomic_fetch_add_u64(&XLogWarmQueue->cancelled, 1); + } + + publish_hand = 0; +} + +/* + * Take the pool out of the way of a relation about to lose its buffers. + * + * DropRelationBuffers() requires that no other process be loading pages of + * the relation into buffers while it runs (bufmgr.c:5081-5083), and a worker + * reading ahead of replay is exactly such a process — one that usually wins + * the race, since its file descriptor keeps reaching an unlinked file and the + * read succeeds. Held across the buffer drop *and* the file operation that + * follows it, this leaves a worker two possibilities and no third: it either + * finished before the drop scanned the pool, in which case the scan removes + * its page, or it starts afterwards and finds the relation gone. + * + * The wait is one page read long, and only relation drops and truncations + * ever wait at all. + */ +void +XLogWarmDropBegin(void) +{ + if (XLogWarmQueue == NULL) + return; + + LWLockAcquire(ReplayWarmReadLock, LW_EXCLUSIVE); + XLogWarmQueue->drop_epoch++; +} + +void +XLogWarmDropEnd(void) +{ + if (XLogWarmQueue != NULL) + LWLockRelease(ReplayWarmReadLock); +} + +/* + * A warmed buffer no longer held the page by the time replay asked for it. + * Only the recent-buffer validation can tell, so it reports it here. + */ +void +XLogWarmCountStale(void) +{ + if (XLogWarmQueue != NULL) + pg_atomic_fetch_add_u64(&XLogWarmQueue->stale, 1); +} + +/* + * Report the pids of the running workers into caller-provided storage of + * XLOGWARM_MAX_WORKERS entries, returning how many were found. + */ +int +XLogWarmGetWorkerPids(int *pids) +{ + int found = 0; + + if (XLogWarmQueue == NULL) + return 0; + + for (int i = 0; i < XLOGWARM_MAX_WORKERS; i++) + { + uint32 pid = pg_atomic_read_u32(&XLogWarmQueue->worker_pids[i]); + + if (pid != 0) + pids[found++] = (int) pid; + } + + return found; +} + +/* + * Read the counters out, for monitoring and for the tests. Returns false + * when the pool is not configured, leaving *stats untouched. + */ +bool +XLogWarmGetStats(XLogWarmStats * stats) +{ + if (XLogWarmQueue == NULL) + return false; + + stats->published = pg_atomic_read_u64(&XLogWarmQueue->published); + stats->dropped_full = pg_atomic_read_u64(&XLogWarmQueue->dropped_full); + stats->collected = pg_atomic_read_u64(&XLogWarmQueue->collected); + stats->missed = pg_atomic_read_u64(&XLogWarmQueue->missed); + stats->stale = pg_atomic_read_u64(&XLogWarmQueue->stale); + stats->cancelled = pg_atomic_read_u64(&XLogWarmQueue->cancelled); + stats->released = pg_atomic_read_u64(&XLogWarmQueue->released); + stats->claimed = pg_atomic_read_u64(&XLogWarmQueue->claimed); + stats->reads = pg_atomic_read_u64(&XLogWarmQueue->reads); + stats->hits = pg_atomic_read_u64(&XLogWarmQueue->hits); + stats->failed = pg_atomic_read_u64(&XLogWarmQueue->failed); + stats->vanished = pg_atomic_read_u64(&XLogWarmQueue->vanished); + stats->discarded = pg_atomic_read_u64(&XLogWarmQueue->discarded); + + return true; +} + +/* + * How many slots are waiting for a worker, and how many a worker holds. + * + * A running total says what the pool has done; this says what it is doing, + * which is what a test needs to arrange anything around a request in flight. + */ +void +XLogWarmGetSlotCounts(int *published, int *claimed) +{ + *published = 0; + *claimed = 0; + + if (XLogWarmQueue == NULL) + return; + + for (int i = 0; i < XLogWarmQueue->capacity; i++) + { + switch (pg_atomic_read_u32(&XLogWarmQueue->slots[i].state)) + { + case XLOGWARM_PUBLISHED: + (*published)++; + break; + case XLOGWARM_CLAIMED: + (*claimed)++; + break; + default: + break; + } + } +} + +/* + * Read one published block into shared buffers. + * + * Runs inside the worker's own resource owner: replay may drop or truncate + * the relation between publication and this read, so any smgr error has to + * be survivable. + */ +static void +XLogWarmDoOne(XLogWarmSlot * slot, uint64 request_id, + RelFileLocator rlocator, ForkNumber forknum, BlockNumber blkno) +{ + SMgrRelation smgr; + Buffer buffer = InvalidBuffer; + uint32 expected; + bool failed = false; + + /* + * A test can park a worker here to hold a slot claimed while it arranges + * what happens next. It sits outside the interlock below on purpose: a + * worker parked while holding that lock would stop replay from dropping + * anything at all. + */ + INJECTION_POINT("replay-warm-before-read", NULL); + + PG_TRY(); + { + /* see XLogWarmDropBegin(): this is the whole reason it exists */ + LWLockAcquire(ReplayWarmReadLock, LW_SHARED); + + /* + * Something lost its files since this worker last looked. Nothing + * tells a process without a database connection that, so it throws + * its own smgr state away and starts from the files as they are now. + */ + if (XLogWarmQueue->drop_epoch != my_drop_epoch) + { + smgrreleaseall(); + my_drop_epoch = XLogWarmQueue->drop_epoch; + } + + smgr = smgropen(rlocator, INVALID_PROC_NUMBER); + + /* + * Re-check what the prefetcher checked when it published: the + * relation may have been dropped or truncated since. Both answers + * come from smgr's cache, so this is cheap. + */ + if (!smgrexists(smgr, forknum) || + blkno >= smgrnblocks(smgr, forknum)) + { + /* + * Replay dropped or truncated the relation between publication + * and now — the ordinary outcome of running ahead of it, and + * the outcome the interlock guarantees for a read that starts + * after a drop. + */ + failed = true; + pg_atomic_fetch_add_u64(&XLogWarmQueue->vanished, 1); + } + else if (BufferIsValid(buffer = LookupSharedBuffer(smgr, forknum, blkno))) + { + /* + * Already resident: not a read, but still the answer replay + * wants, so hand the buffer on as if we had read it. + */ + pg_atomic_fetch_add_u64(&XLogWarmQueue->hits, 1); + } + else + { + buffer = ReadBufferWithoutRelcache(rlocator, forknum, blkno, + RBM_NORMAL, NULL, true); + pg_atomic_fetch_add_u64(&XLogWarmQueue->reads, 1); + + /* + * Hand the buffer number on and let go: holding pins ahead of + * replay would pin down a slice of the buffer pool, and replay + * validates the hint anyway. + */ + ReleaseBuffer(buffer); + } + + LWLockRelease(ReplayWarmReadLock); + } + PG_CATCH(); + { + /* an unreadable block is not this pool's problem to solve */ + buffer = InvalidBuffer; + failed = true; + pg_atomic_fetch_add_u64(&XLogWarmQueue->failed, 1); + + MemoryContextSwitchTo(TopMemoryContext); + FlushErrorState(); + + /* + * Whatever the failed read was holding goes back here: the + * lightweight locks it took, including the interlock above, and then + * the pins and the buffer I/O owned by the aux-process resource + * owner. Releasing the owner is what repairs a read interrupted + * mid-flight — it hands the buffer's I/O back, so whoever waits on + * that buffer can retry instead of waiting on a process that is no + * longer reading. + */ + LWLockReleaseAll(); + ReleaseAuxProcessResources(false); + } + PG_END_TRY(); + + slot->result_id = request_id; + slot->result_buffer = buffer; + + /* the result must be visible before the state that advertises it */ + pg_write_barrier(); + + /* + * The slot is ours until we leave it, so this compare-and-swap is a + * statement of that invariant rather than a race to win; a failure would + * mean somebody else wrote a claimed slot, and the counter says so. + */ + expected = XLOGWARM_CLAIMED; + if (!pg_atomic_compare_exchange_u32(&slot->state, &expected, + failed ? XLOGWARM_FAILED : XLOGWARM_DONE)) + pg_atomic_fetch_add_u64(&XLogWarmQueue->discarded, 1); +} + +/* + * Leave the pool tidily. + * + * Two things outlive this process if it does not: the pid it advertised, + * which would point at a process that no longer exists, and the slot it + * holds, which no one else may write and which would therefore shrink the + * ring for the rest of the cluster's life. This runs on the way out of a + * signalled or FATAL exit — the paths that leave shared memory in place. + */ +static void +XLogWarmWorkerExit(int code, Datum arg) +{ + int worker_id = DatumGetInt32(arg); + + if (XLogWarmQueue == NULL) + return; + + if (my_claimed_slot >= 0) + { + XLogWarmSlot *slot = &XLogWarmQueue->slots[my_claimed_slot]; + uint32 expected = XLOGWARM_CLAIMED; + + my_claimed_slot = -1; + + if (pg_atomic_compare_exchange_u32(&slot->state, &expected, + XLOGWARM_FAILED)) + pg_atomic_fetch_add_u64(&XLogWarmQueue->released, 1); + } + + pg_atomic_write_u32(&XLogWarmQueue->worker_pids[worker_id], 0); +} + +/* + * Claim and serve one published slot. Returns false when the ring holds + * nothing to do. + */ +static bool +XLogWarmServeOne(void) +{ + int capacity = XLogWarmQueue->capacity; + uint32 start = pg_atomic_fetch_add_u32(&XLogWarmQueue->hand, 1); + + for (int i = 0; i < capacity; i++) + { + XLogWarmSlot *slot = &XLogWarmQueue->slots[(start + i) % capacity]; + uint32 expected = XLOGWARM_PUBLISHED; + uint64 request_id; + RelFileLocator rlocator; + ForkNumber forknum; + BlockNumber blkno; + + if (!pg_atomic_compare_exchange_u32(&slot->state, &expected, + XLOGWARM_CLAIMED)) + continue; + + /* the state was observed before the payload it advertises */ + pg_read_barrier(); + + request_id = slot->request_id; + rlocator = slot->rlocator; + forknum = slot->forknum; + blkno = slot->blkno; + + /* + * From here until the slot is finished this worker owns it, and says + * so where its exit callback can see it. + */ + my_claimed_slot = (start + i) % capacity; + + pg_atomic_fetch_add_u64(&XLogWarmQueue->claimed, 1); + XLogWarmDoOne(slot, request_id, rlocator, forknum, blkno); + + my_claimed_slot = -1; + return true; + } + + return false; +} + +/* + * Register the pool. Like the DWB cleaner pool, a worker slot shortage is + * fatal rather than silent: a smaller pool than the operator configured is + * a performance surprise nobody asked for. + */ +void +XLogWarmWorkersRegister(void) +{ + BackgroundWorker bgw; + int free_slots; + + if (replay_warm_workers == 0) + return; + + free_slots = max_worker_processes - GetNumRegisteredBackgroundWorkers(); + if (replay_warm_workers > free_slots) + ereport(FATAL, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"replay_warm_workers\" (%d) needs more \"max_worker_processes\" slots than remain free (%d)", + replay_warm_workers, free_slots), + errhint("Increase \"max_worker_processes\" or decrease \"replay_warm_workers\"."))); + + for (int i = 0; i < replay_warm_workers; i++) + { + memset(&bgw, 0, sizeof(bgw)); + + /* + * No database connection: the workers deal in relation locators and + * shared buffers only. That also lets them start at postmaster + * start, so they serve crash recovery on a primary from the first + * record, not only a standby past consistency. + */ + bgw.bgw_flags = BGWORKER_SHMEM_ACCESS; + bgw.bgw_start_time = BgWorkerStart_PostmasterStart; + snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres"); + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "XLogWarmWorkerMain"); + snprintf(bgw.bgw_name, BGW_MAXLEN, "replay warm worker %d", i); + snprintf(bgw.bgw_type, BGW_MAXLEN, "replay warm worker"); + bgw.bgw_restart_time = 1; + bgw.bgw_notify_pid = 0; + bgw.bgw_main_arg = Int32GetDatum(i); + + RegisterBackgroundWorker(&bgw); + } +} + +/* + * Main loop: serve published slots, sleep when there is nothing published. + */ +void +XLogWarmWorkerMain(Datum main_arg) +{ + int worker_id; + + pqsignal(SIGHUP, SignalHandlerForConfigReload); + + /* + * die, not a shutdown flag: a worker exiting with code 0 is unregistered + * for good, so one stray SIGTERM would permanently shrink the pool. + */ + pqsignal(SIGTERM, die); + BackgroundWorkerUnblockSignals(); + + /* + * ReadBufferWithoutRelcache pins buffers and registers its buffer I/O + * with CurrentResourceOwner; that registration is what releases an + * interrupted read if an ERROR throws the worker out of a slot. The + * aux-process owner provides both the owner and its shmem-exit release. + */ + CreateAuxProcessResourceOwner(); + + Assert(XLogWarmQueue != NULL); + + worker_id = DatumGetInt32(main_arg); + Assert(worker_id >= 0 && worker_id < XLOGWARM_MAX_WORKERS); + pg_atomic_write_u32(&XLogWarmQueue->worker_pids[worker_id], MyProcPid); + before_shmem_exit(XLogWarmWorkerExit, Int32GetDatum(worker_id)); + + for (;;) + { + /* the CFI is what turns a pending die() into the FATAL exit */ + CHECK_FOR_INTERRUPTS(); + + if (ConfigReloadPending) + { + ConfigReloadPending = false; + ProcessConfigFile(PGC_SIGHUP); + } + + if (!XLogWarmServeOne()) + { + /* + * Sleep without losing a wakeup: get onto the wait list first, + * then recheck, then sleep. A signal sent after the recheck is + * kept by the prepared state; a request published before it is + * seen by the recheck. + */ + ConditionVariablePrepareToSleep(&XLogWarmQueue->cv_work); + if (!XLogWarmServeOne()) + { + ConditionVariableSleep(&XLogWarmQueue->cv_work, + WAIT_EVENT_REPLAY_WARM_MAIN); + continue; + } + } + + /* off the wait list while serving (no-op if never prepared) */ + ConditionVariableCancelSleep(); + } +} diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c index 5eb6caffe6d1a..c2610d565fef6 100644 --- a/src/backend/commands/dbcommands.c +++ b/src/backend/commands/dbcommands.c @@ -32,6 +32,7 @@ #include "access/xloginsert.h" #include "access/xlogrecovery.h" #include "access/xlogutils.h" +#include "access/xlogwarm.h" #include "catalog/catalog.h" #include "catalog/dependency.h" #include "catalog/indexing.h" @@ -1851,6 +1852,14 @@ dropdb(const char *dbname, bool missing_ok, bool force) */ ReplicationSlotsDropDBSlots(db_id); + /* + * As in dbase_redo(): hold the replay warm pool off until the files are + * gone. A worker that was still finishing a read when recovery ended + * could otherwise put a page of this database back into the buffer pool + * after the drop below (see XLogWarmDropBegin()). + */ + XLogWarmDropBegin(); + /* * Drop pages for this database that are in the shared buffer cache. This * is important to ensure that no remaining backend tries to write out a @@ -1880,6 +1889,8 @@ dropdb(const char *dbname, bool missing_ok, bool force) */ remove_dbtablespaces(db_id); + XLogWarmDropEnd(); + /* * Close pg_database, but keep lock till commit. */ @@ -3431,6 +3442,13 @@ dbase_redo(XLogReaderState *record) /* Drop any database-specific replication slots */ ReplicationSlotsDropDBSlots(xlrec->db_id); + /* + * Keep the replay warm pool away until the directories are gone, so a + * worker cannot load a page of this database into buffers behind the + * drop below (see XLogWarmDropBegin()). + */ + XLogWarmDropBegin(); + /* Drop pages for this database that are in the shared buffer cache */ DropDatabaseBuffers(xlrec->db_id); @@ -3455,6 +3473,8 @@ dbase_redo(XLogReaderState *record) pfree(dst_path); } + XLogWarmDropEnd(); + if (InHotStandby) { /* diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c index ec766f1a2bcf4..fe49d10783f60 100644 --- a/src/backend/postmaster/bgworker.c +++ b/src/backend/postmaster/bgworker.c @@ -21,6 +21,7 @@ #include "postmaster/postmaster.h" #include "replication/logicallauncher.h" #include "replication/logicalworker.h" +#include "access/xlogwarm.h" #include "storage/dwb.h" #include "storage/ipc.h" #include "storage/latch.h" @@ -131,6 +132,9 @@ static const struct { "DWBCleanerWorkerMain", DWBCleanerWorkerMain }, + { + "XLogWarmWorkerMain", XLogWarmWorkerMain + }, { "ApplyWorkerMain", ApplyWorkerMain }, diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index c3e98905f7227..0ae9718f3fb92 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -110,6 +110,7 @@ #include "replication/slotsync.h" #include "replication/walsender.h" #include "storage/aio_subsys.h" +#include "access/xlogwarm.h" #include "storage/dwb.h" #include "storage/fd.h" #include "storage/io_worker.h" @@ -937,6 +938,13 @@ PostmasterMain(int argc, char *argv[]) /* And the double write buffer cleaner pool feeding off the bgwriter. */ DWBCleanerWorkersRegister(); + /* + * The replay warm pool, which fetches pages ahead of redo. It takes no + * database connection, so it can start now and serve crash recovery from + * the first record. + */ + XLogWarmWorkersRegister(); + /* * process any libraries that should be preloaded at postmaster start */ diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index d982e71b44e95..709b4c9635f7c 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -561,14 +561,22 @@ static int ts_ckpt_progress_comparator(Datum a, Datum b, void *arg); /* - * Implementation of PrefetchBuffer() for shared buffers. + * Look up a shared buffer without touching storage. + * + * Returns the buffer the block was found in, or InvalidBuffer. As with + * PrefetchSharedBuffer(), the buffer is not pinned and the answer is only a + * hint: the caller must recheck, typically through ReadRecentBuffer(). + * + * Callers that want the residency answer alone use this: the recovery + * prefetcher, to decide whether a block is worth handing to the warm pool, + * and a warm worker, to tell a real read from a hit (ReadBufferWithoutRelcache + * does not report that). */ -PrefetchBufferResult -PrefetchSharedBuffer(SMgrRelation smgr_reln, - ForkNumber forkNum, - BlockNumber blockNum) +Buffer +LookupSharedBuffer(SMgrRelation smgr_reln, + ForkNumber forkNum, + BlockNumber blockNum) { - PrefetchBufferResult result = {InvalidBuffer, false}; BufferTag newTag; /* identity of requested block */ uint32 newHash; /* hash value for newTag */ LWLock *newPartitionLock; /* buffer partition lock for it */ @@ -589,8 +597,24 @@ PrefetchSharedBuffer(SMgrRelation smgr_reln, buf_id = BufTableLookup(&newTag, newHash); LWLockRelease(newPartitionLock); + return buf_id < 0 ? InvalidBuffer : buf_id + 1; +} + +/* + * Implementation of PrefetchBuffer() for shared buffers. + */ +PrefetchBufferResult +PrefetchSharedBuffer(SMgrRelation smgr_reln, + ForkNumber forkNum, + BlockNumber blockNum) +{ + PrefetchBufferResult result = {InvalidBuffer, false}; + Buffer recent_buffer; + + recent_buffer = LookupSharedBuffer(smgr_reln, forkNum, blockNum); + /* If not in buffers, initiate prefetch */ - if (buf_id < 0) + if (!BufferIsValid(recent_buffer)) { #ifdef USE_PREFETCH /* @@ -611,7 +635,7 @@ PrefetchSharedBuffer(SMgrRelation smgr_reln, * to avoid a buffer table lookup, but it's not pinned and it must be * rechecked! */ - result.recent_buffer = buf_id + 1; + result.recent_buffer = recent_buffer; } /* @@ -5424,6 +5448,7 @@ DropDatabaseBuffers(Oid dbid) * database isn't our own. */ + for (i = 0; i < NBuffers; i++) { BufferDesc *bufHdr = GetBufferDescriptor(i); diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index fd8f78ac1dedb..7a8fef25d5031 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -39,6 +39,7 @@ #include "replication/walsender.h" #include "storage/aio_subsys.h" #include "storage/bufmgr.h" +#include "access/xlogwarm.h" #include "storage/dwb.h" #include "storage/dsm.h" #include "storage/dsm_registry.h" @@ -116,6 +117,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, DSMRegistryShmemSize()); size = add_size(size, BufferManagerShmemSize()); size = add_size(size, DWBShmemSize()); + size = add_size(size, XLogWarmShmemSize()); size = add_size(size, LockManagerShmemSize()); size = add_size(size, PredicateLockShmemSize()); size = add_size(size, ProcGlobalShmemSize()); @@ -296,6 +298,7 @@ CreateOrAttachShmemStructs(void) MultiXactShmemInit(); BufferManagerShmemInit(); DWBShmemInit(); + XLogWarmShmemInit(); /* * Set up lock manager diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c index 37b99fa319852..591faf22892db 100644 --- a/src/backend/storage/smgr/smgr.c +++ b/src/backend/storage/smgr/smgr.c @@ -551,6 +551,14 @@ smgrdounlinkall(SMgrRelation *rels, int nrels, bool isRedo) */ HOLD_INTERRUPTS(); + /* + * Keep the replay warm pool out of these relations until the files are + * gone: a worker reading one of their pages right now would otherwise + * leave that page in the buffer pool, which is exactly what + * DropRelationsAllBuffers() must not have happen behind it. + */ + XLogWarmDropBegin(); + /* * Get rid of any remaining buffers for the relations. bufmgr will just * drop them without bothering to write the contents. @@ -603,6 +611,8 @@ smgrdounlinkall(SMgrRelation *rels, int nrels, bool isRedo) pfree(rlocators); + XLogWarmDropEnd(); + RESUME_INTERRUPTS(); } @@ -877,6 +887,13 @@ smgrtruncate(SMgrRelation reln, ForkNumber *forknum, int nforks, { int i; + /* + * As in smgrdounlinkall(): a warm-pool worker must not be able to load a + * page of this relation into buffers between the drop below and the + * truncation that follows it. + */ + XLogWarmDropBegin(); + /* * Get rid of any buffers for the about-to-be-deleted blocks. bufmgr will * just drop them without bothering to write the contents. @@ -922,6 +939,8 @@ smgrtruncate(SMgrRelation reln, ForkNumber *forknum, int nforks, reln->smgr_cached_nblocks[forknum[i]] = nblocks[i] > old_nblocks[i] ? old_nblocks[i] : nblocks[i]; } + + XLogWarmDropEnd(); } /* diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index c9ab8ca178f09..2d0b75d0964e2 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -64,6 +64,7 @@ LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process." LOGICAL_LAUNCHER_MAIN "Waiting in main loop of logical replication launcher process." LOGICAL_PARALLEL_APPLY_MAIN "Waiting in main loop of logical replication parallel apply process." RECOVERY_WAL_STREAM "Waiting in main loop of startup process for WAL to arrive, during streaming recovery." +REPLAY_WARM_MAIN "Waiting in main loop of a replay warm worker." REPLICATION_SLOTSYNC_MAIN "Waiting in main loop of slot sync worker." REPLICATION_SLOTSYNC_SHUTDOWN "Waiting for slot sync worker to shut down." SYSLOGGER_MAIN "Waiting in main loop of syslogger process." @@ -370,6 +371,7 @@ DWBSegHash "Waiting to read or update the double write buffer segment hash table DWBSelfSweep "Waiting to run the double write buffer self-help retirement sweep." DWBSyncfsRound "Waiting to run a wholesale double write buffer retirement round." DWBCleanerQueue "Waiting to access the double write buffer cleaner work queue." +ReplayWarmRead "Waiting for replay prefetch workers to finish reading pages of a relation being dropped or truncated." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index d0758786c99b4..d8d888241ad3b 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -37,6 +37,7 @@ #include "access/xlog_internal.h" #include "access/xlogprefetcher.h" #include "access/xlogrecovery.h" +#include "access/xlogwarm.h" #include "access/xlogutils.h" #include "archive/archive_module.h" #include "catalog/namespace.h" @@ -2249,6 +2250,30 @@ struct config_int ConfigureNamesInt[] = 0, 0, 64, NULL, NULL, NULL }, + { + {"replay_warm_workers", PGC_POSTMASTER, WAL_RECOVERY, + gettext_noop("Number of replay warm worker processes."), + gettext_noop("The pool reads the pages replay is about to modify " + "into shared buffers ahead of it, which matters when " + "the WAL stream carries no full-page images. The " + "workers consume \"max_worker_processes\" slots. 0 " + "disables the pool and recovery prefetching falls " + "back to advising the operating system.") + }, + &replay_warm_workers, + 0, 0, 64, + NULL, NULL, NULL + }, + { + {"replay_warm_queue_size", PGC_POSTMASTER, WAL_RECOVERY, + gettext_noop("Number of block requests the replay warm pool can hold."), + gettext_noop("This also bounds how far ahead of replay the " + "prefetcher looks when the pool is enabled.") + }, + &replay_warm_queue_size, + 256, 16, 8192, + NULL, NULL, NULL + }, { {"dwb_batch_timeout_ms", PGC_SIGHUP, WAL_SETTINGS, gettext_noop("Maximum time an open double write buffer batch may wait before being sealed."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 1875b61109a64..e2109389458b1 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -298,6 +298,10 @@ #recovery_prefetch = try # prefetch pages referenced in the WAL? #wal_decode_buffer_size = 512kB # lookahead window used for prefetching # (change requires restart) +#replay_warm_workers = 0 # workers reading pages ahead of replay + # (change requires restart) +#replay_warm_queue_size = 256 # block requests the warm pool can hold + # (change requires restart) # - Archiving - diff --git a/src/include/access/xlogreader.h b/src/include/access/xlogreader.h index 9738462d3c9f1..3dadff389d432 100644 --- a/src/include/access/xlogreader.h +++ b/src/include/access/xlogreader.h @@ -116,6 +116,9 @@ typedef struct XLogReaderRoutine #define XL_ROUTINE(...) &(XLogReaderRoutine){__VA_ARGS__} +/* a block that was not published to the replay warm pool */ +#define XLOGWARM_NO_SLOT (-1) + typedef struct { /* Is this block ref in use? */ @@ -129,6 +132,23 @@ typedef struct /* Prefetching workspace. */ Buffer prefetch_buffer; + /* + * Warm pool workspace: the slot this block was published to and the + * request it was published as, so the answer can be told apart from a + * later request that recycled the slot. XLOGWARM_NO_SLOT when the block + * was never published (which is always the case in frontend code). + */ + int warm_slot; + uint64 warm_request; + + /* + * True when prefetch_buffer above is an answer collected from the pool + * rather than a buffer the cache lookup happened to find. Only the + * pool's own answers say anything about the pool when they turn out to be + * stale. + */ + bool warm_hint; + /* copy of the fork_flags field from the XLogRecordBlockHeader */ uint8 flags; diff --git a/src/include/access/xlogwarm.h b/src/include/access/xlogwarm.h new file mode 100644 index 0000000000000..a68ceca293a3c --- /dev/null +++ b/src/include/access/xlogwarm.h @@ -0,0 +1,62 @@ +/*------------------------------------------------------------------------- + * + * xlogwarm.h + * Replay prefetch worker pool. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/access/xlogwarm.h + *------------------------------------------------------------------------- + */ +#ifndef XLOGWARM_H +#define XLOGWARM_H + +#include "access/xlogreader.h" +#include "storage/block.h" +#include "storage/bufmgr.h" +#include "storage/relfilelocator.h" + +/* GUCs */ +extern PGDLLIMPORT int replay_warm_workers; +extern PGDLLIMPORT int replay_warm_queue_size; + +/* a snapshot of the pool's counters */ +typedef struct XLogWarmStats +{ + uint64 published; /* blocks handed to the pool */ + uint64 dropped_full; /* blocks left unwarmed, no free slot */ + uint64 collected; /* answers replay picked up */ + uint64 missed; /* requests with no answer to pick up */ + uint64 stale; /* answers whose buffer had been evicted */ + uint64 cancelled; /* requests withdrawn on a prefetcher reset */ + uint64 released; /* slots handed back by a departing worker */ + uint64 claimed; /* requests a worker took */ + uint64 reads; /* pages a worker actually read */ + uint64 hits; /* pages already resident when claimed */ + uint64 failed; /* reads that errored out */ + uint64 vanished; /* relations gone or too short by read time */ + uint64 discarded; /* results dropped, slot no longer theirs */ +} XLogWarmStats; + +extern Size XLogWarmShmemSize(void); +extern void XLogWarmShmemInit(void); +extern void XLogWarmWorkersRegister(void); +pg_noreturn extern void XLogWarmWorkerMain(Datum main_arg); + +extern bool XLogWarmPoolActive(void); +extern int XLogWarmPublish(RelFileLocator rlocator, ForkNumber forknum, + BlockNumber blkno, uint64 *request_id); +extern Buffer XLogWarmCollect(int slot_no, uint64 request_id); +extern void XLogWarmCancelAll(void); +extern void XLogWarmCountStale(void); +extern void XLogWarmDropBegin(void); +extern void XLogWarmDropEnd(void); +extern bool XLogWarmGetStats(XLogWarmStats * stats); +extern void XLogWarmGetSlotCounts(int *published, int *claimed); +extern int XLogWarmGetWorkerPids(int *pids); + +/* the pool size ceiling, matching the setting's maximum */ +#define XLOGWARM_MAX_WORKERS 64 + +#endif /* XLOGWARM_H */ diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h index 41fdc1e76938e..988d81a8f543c 100644 --- a/src/include/storage/bufmgr.h +++ b/src/include/storage/bufmgr.h @@ -201,6 +201,9 @@ extern PGDLLIMPORT int32 *LocalRefCount; /* * prototypes for functions in bufmgr.c */ +extern Buffer LookupSharedBuffer(struct SMgrRelationData *smgr_reln, + ForkNumber forkNum, + BlockNumber blockNum); extern PrefetchBufferResult PrefetchSharedBuffer(struct SMgrRelationData *smgr_reln, ForkNumber forkNum, BlockNumber blockNum); diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 9f331951e255c..bee80d842c7c2 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -89,3 +89,4 @@ PG_LWLOCK(55, DWBSegHash) PG_LWLOCK(56, DWBSelfSweep) PG_LWLOCK(57, DWBSyncfsRound) PG_LWLOCK(58, DWBCleanerQueue) +PG_LWLOCK(59, ReplayWarmRead) diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index bd0e331a44e86..3daab16c9e913 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -57,6 +57,7 @@ tests += { 't/018_cleaners.pl', 't/019_autovacuum_class.pl', 't/020_ckpt_yield.pl', + 't/021_replay_warm.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/021_replay_warm.pl b/src/test/modules/test_dwb/t/021_replay_warm.pl new file mode 100644 index 0000000000000..4d8e4ac047296 --- /dev/null +++ b/src/test/modules/test_dwb/t/021_replay_warm.pl @@ -0,0 +1,466 @@ + +# Copyright (c) 2025, PostgreSQL Global Development Group + +# The replay warm pool: with no full-page images in the stream, a standby +# has to fetch every page it replays, and this pool does that fetching in +# background workers instead of in the startup process. The scenarios +# here check that the pool is actually used, that over one and the same +# stretch of WAL it takes reads off the startup process, that a request a +# worker is holding survives the relation being dropped, that a worker +# killed while holding a request gives the slot back, that the pool works +# on its own with the kernel-advice prefetcher turned off, and that +# promotion with requests still outstanding is clean. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; +use Time::HiRes qw(usleep); + +my $injection_points = defined $ENV{enable_injection_points} + && $ENV{enable_injection_points} eq 'yes'; + +# The workload must not fit in the standby's buffer cache, or replay would +# find every page resident and the pool would have nothing to do. +my $primary = PostgreSQL::Test::Cluster->new('warm_primary'); +$primary->init(allows_streaming => 1); +$primary->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 1 +shared_buffers = 2MB +autovacuum = off +fsync = off +wal_keep_size = 256MB +)); +$primary->start; +$primary->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); + +# The standbys park their workers at an injection point, so the extension +# has to exist in their catalogs; a standby cannot create it itself. +$primary->safe_psql('postgres', 'CREATE EXTENSION injection_points') + if $injection_points; + +# --- a standby with the pool enabled ------------------------------------ + +$primary->backup('bkp'); + +sub make_standby +{ + my ($name, $warm_workers, %extra) = @_; + my $node = PostgreSQL::Test::Cluster->new($name); + $node->init_from_backup($primary, 'bkp', has_streaming => 1); + $node->append_conf( + 'postgresql.conf', qq( +shared_buffers = 2MB +max_worker_processes = 16 +replay_warm_workers = $warm_workers +replay_warm_queue_size = 64 +)); + # The workers hold no database connection, so a lazily loaded injection + # point would palloc outside a transaction; preloading gives them the + # library from the postmaster instead. + $node->append_conf('postgresql.conf', + "shared_preload_libraries = 'injection_points'") + if $injection_points; + $node->append_conf('postgresql.conf', $extra{conf}) if $extra{conf}; + return $node; +} + +my $standby = make_standby('warm_standby', 2); +$standby->start; + +# The workers hold no database connection, so they have no pg_stat_activity +# row; they advertise themselves in shared memory instead. +$standby->poll_query_until('postgres', + 'SELECT count(*) = 2 FROM test_dwb_warm_worker_pids()') + or die 'timed out waiting for the warm workers to start'; +pass('both warm workers are running'); + +# a table several times the buffer cache, so replay must fetch pages +$primary->safe_psql( + 'postgres', q( + CREATE TABLE t AS + SELECT g AS id, repeat('x', 200) AS filler + FROM generate_series(1, 40000) g; + CHECKPOINT; +)); +$primary->wait_for_catchup($standby, 'replay'); +$primary->safe_psql('postgres', + "UPDATE t SET filler = repeat('y', 200) WHERE id % 3 = 0"); +$primary->wait_for_catchup($standby, 'replay'); + +sub warm_counters +{ + my ($node) = @_; + my %c; + @c{ + qw(published dropped_full collected missed stale cancelled + released claimed reads hits failed discarded vanished) + } + = split /\|/, + $node->safe_psql('postgres', 'SELECT * FROM test_dwb_warm_counters()'); + return \%c; +} + +my $warm = warm_counters($standby); + +cmp_ok($warm->{published}, '>', 0, 'blocks were published to the warm pool'); +cmp_ok($warm->{claimed}, '>', 0, 'workers claimed published blocks'); +cmp_ok($warm->{reads}, '>', 0, 'workers read pages for replay'); +cmp_ok($warm->{collected}, '>', 0, + 'replay collected pages the workers had read'); +is($warm->{discarded}, 0, 'no worker lost its slot under a healthy run'); + +# --- the same stretch of WAL, with the pool and without ----------------- + +# One wakeup releases one waiter, and a waiter clears its registration only +# once it runs, so waking a parked pool takes as many wakeups as it takes: +# keep at it until no worker holds a request any more. Detach the point +# first, or a woken worker parks again on its next request. +sub wake_parked_workers +{ + my ($node) = @_; + + foreach my $attempt (1 .. 300) + { + return + if $node->safe_psql('postgres', + 'SELECT claimed FROM test_dwb_warm_slot_states()') == 0; + + # not safe_psql: with the last waiter already gone this errors out + $node->psql('postgres', + "SELECT injection_points_wakeup('replay-warm-before-read')"); + usleep(100_000); + } + die 'the parked workers would not wake up'; +} + +sub startup_reads +{ + my ($node) = @_; + return $node->safe_psql( + 'postgres', + q(SELECT coalesce(sum(reads), 0) FROM pg_stat_io + WHERE backend_type = 'startup' AND object = 'relation')); +} + +# Both standbys are caught up to the same point before the measured +# statement, and both are measured by their own increment over it, so the +# two numbers cover one and the same WAL. +# +# A startup process reports its statistics when it replays a running-xacts +# record, which a checkpoint emits; without one on each side of the +# measured statement the numbers would be whatever happened to have been +# reported by then. +my $plain = make_standby('plain_standby', 0); +$plain->start; +$primary->safe_psql('postgres', 'CHECKPOINT'); +$primary->wait_for_catchup($plain, 'replay'); +$primary->wait_for_catchup($standby, 'replay'); + +my $warm_reads_before = startup_reads($standby); +my $plain_reads_before = startup_reads($plain); + +$primary->safe_psql('postgres', + "UPDATE t SET filler = repeat('z', 200) WHERE id % 3 = 1"); +$primary->safe_psql('postgres', 'CHECKPOINT'); +$primary->wait_for_catchup($plain, 'replay'); +$primary->wait_for_catchup($standby, 'replay'); + +$plain->poll_query_until( + 'postgres', + "SELECT coalesce(sum(reads), 0) > $plain_reads_before FROM pg_stat_io + WHERE backend_type = 'startup' AND object = 'relation'" +) or die 'timed out waiting for the unaided standby to report its reads'; + +my $warm_delta = startup_reads($standby) - $warm_reads_before; +my $plain_delta = startup_reads($plain) - $plain_reads_before; + +cmp_ok($plain_delta, '>', 0, + 'an unaided startup process reads pages over this WAL'); +cmp_ok($warm_delta, '<', $plain_delta, + 'the warm pool keeps reads off the startup process'); +$plain->stop; + +# --- a relation dropped while a worker holds a request for it ----------- + +SKIP: +{ + skip 'injection points not supported by this build', 8 + unless $injection_points; + + # The table exists on both sides before the pool is parked, so the only + # WAL left in flight — and so the only thing the parked workers can be + # holding — is the update of the table about to be dropped. + $primary->safe_psql( + 'postgres', q( + CREATE TABLE doomed AS + SELECT g AS id, repeat('d', 200) AS filler + FROM generate_series(1, 20000) g; + CHECKPOINT; + )); + $primary->wait_for_catchup($standby, 'replay'); + + # First make the workers read this table for real, so each of them holds + # open segments and a cached size for it. Creating the table would not + # have done that: its pages arrive as initialised pages, which replay + # never needs read. A worker still carrying that state into the drop is + # what the scenario is about. + my $reads_before_doomed = warm_counters($standby)->{reads}; + $primary->safe_psql('postgres', + "UPDATE doomed SET filler = repeat('e', 200) WHERE id % 2 = 0"); + $primary->wait_for_catchup($standby, 'replay'); + cmp_ok(warm_counters($standby)->{reads}, + '>', $reads_before_doomed, + 'the workers read the table before it is doomed'); + + $standby->safe_psql('postgres', + "SELECT injection_points_attach('replay-warm-before-read', 'wait')"); + + $primary->safe_psql('postgres', + "UPDATE doomed SET filler = repeat('f', 200) WHERE id % 2 = 1"); + + $standby->poll_query_until('postgres', + 'SELECT claimed = 2 FROM test_dwb_warm_slot_states()') + or die 'timed out waiting for the workers to hold requests'; + pass('both workers are parked holding a request for the doomed table'); + + my $doomed_file = $primary->safe_psql('postgres', + "SELECT relfilenode FROM pg_class WHERE relname = 'doomed'"); + my $vanished_before = warm_counters($standby)->{vanished}; + + # Replay never waits for the pool, so it drops the relation out from + # under the parked request and moves on. + $primary->safe_psql('postgres', 'DROP TABLE doomed'); + $primary->wait_for_catchup($standby, 'replay'); + pass('replay went past the drop while a request was held'); + + $standby->safe_psql('postgres', + "SELECT injection_points_detach('replay-warm-before-read')"); + wake_parked_workers($standby); + + # Each woken worker starts its read after the drop has finished, which is + # the interesting order: the interlock makes it find the relation gone + # rather than read a page through a descriptor that still reaches the + # unlinked file. + $standby->poll_query_until('postgres', + "SELECT vanished > $vanished_before FROM test_dwb_warm_counters()") + or die 'timed out waiting for the held request to fail on the drop'; + pass('a read starting after the drop finds the relation gone'); + + $standby->poll_query_until('postgres', + 'SELECT claimed = 0 FROM test_dwb_warm_slot_states()') + or die 'timed out waiting for the workers to let their slots go'; + pass('the workers gave their slots back'); + + is( $standby->safe_psql( + 'postgres', "SELECT test_dwb_count_rel_buffers($doomed_file)"), + 0, + 'no buffer was left behind for the dropped relation'); + + # a pool that only ever fails would be silently useless + my $reads_before = warm_counters($standby)->{reads}; + $primary->safe_psql('postgres', + "UPDATE t SET filler = repeat('k', 200) WHERE id % 3 = 2"); + $primary->wait_for_catchup($standby, 'replay'); + cmp_ok(warm_counters($standby)->{reads}, + '>', $reads_before, 'the pool kept reading after the drop'); + + is(warm_counters($standby)->{discarded}, + 0, 'no result was written into a slot its worker had lost'); +} + +# --- a relation truncated under a request for a block past its new end -- + +SKIP: +{ + skip 'injection points not supported by this build', 4 + unless $injection_points; + + $primary->safe_psql( + 'postgres', q( + CREATE TABLE shrunk AS + SELECT g AS id, repeat('s', 200) AS filler + FROM generate_series(1, 20000) g; + CHECKPOINT; + )); + $primary->wait_for_catchup($standby, 'replay'); + + # Warm the workers on the full-length relation, so each of them is + # carrying its size from before the truncation. + my $reads_before = warm_counters($standby)->{reads}; + $primary->safe_psql('postgres', + "UPDATE shrunk SET filler = repeat('u', 200) WHERE id % 2 = 0"); + $primary->wait_for_catchup($standby, 'replay'); + cmp_ok(warm_counters($standby)->{reads}, + '>', $reads_before, + 'the workers read the relation at its full length'); + + # Park them on blocks near the end — the part about to be cut off. + $standby->safe_psql('postgres', + "SELECT injection_points_attach('replay-warm-before-read', 'wait')"); + $primary->safe_psql('postgres', + "UPDATE shrunk SET filler = repeat('v', 200) WHERE id > 18000"); + $standby->poll_query_until('postgres', + 'SELECT claimed = 2 FROM test_dwb_warm_slot_states()') + or die 'timed out waiting for the workers to hold requests'; + + my $vanished_before = warm_counters($standby)->{vanished}; + my $failed_before = warm_counters($standby)->{failed}; + # Vacuum gives back the empty tail. Every row goes, so the file ends up + # empty and every parked request points past its end — with rows left + # behind, the tail could still hold live versions of them and the + # truncation would stop short of the blocks the workers are holding. + $primary->safe_psql('postgres', 'DELETE FROM shrunk'); + $primary->safe_psql('postgres', 'VACUUM shrunk'); + $primary->wait_for_catchup($standby, 'replay'); + + is( $standby->safe_psql('postgres', "SELECT pg_relation_size('shrunk')"), + 0, + 'replay truncated the relation away while requests were held'); + + $standby->safe_psql('postgres', + "SELECT injection_points_detach('replay-warm-before-read')"); + wake_parked_workers($standby); + + # The size each worker remembers is from before the truncation, and + # nothing tells a process without a database connection to forget it — + # except the pool itself, which is what this asserts: the block is + # recognised as past the end instead of being read against a stale size. + $standby->poll_query_until('postgres', + "SELECT vanished > $vanished_before FROM test_dwb_warm_counters()") + or die + 'timed out waiting for the held requests to notice the truncation' + . '; counters: ' + . $standby->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_counters()') + . " (vanished was $vanished_before)"; + pass('a request for a block past the new end is dropped, not read'); + + # wake_parked_workers() returned only once no worker held a request, so + # every parked request has been dealt with by now. A worker that carried + # its old size past the check would have gone on to read past the end of + # the file and errored out, which lands in a different counter; that this + # one did not move is what says the size was refreshed rather than the + # read merely failing somewhere else. + is(warm_counters($standby)->{failed}, + $failed_before, 'no worker read against the size it remembered'); +} + +# --- a worker killed while it holds a request --------------------------- + +SKIP: +{ + skip 'injection points not supported by this build', 3 + unless $injection_points; + + $standby->safe_psql('postgres', + "SELECT injection_points_attach('replay-warm-before-read', 'wait')"); + + $primary->safe_psql('postgres', + "UPDATE t SET filler = repeat('m', 200) WHERE id % 5 = 0"); + $standby->poll_query_until('postgres', + 'SELECT claimed = 2 FROM test_dwb_warm_slot_states()') + or die 'timed out waiting for the workers to hold requests'; + + my $released_before = warm_counters($standby)->{released}; + my $victim = $standby->safe_psql('postgres', + 'SELECT pid FROM test_dwb_warm_worker_pids() ORDER BY worker LIMIT 1' + ); + kill 'TERM', $victim; + + $standby->poll_query_until('postgres', + "SELECT released > $released_before FROM test_dwb_warm_counters()") + or die 'the killed worker did not give its slot back'; + pass('a worker killed mid-request gives its slot back'); + + # The killed worker never got to clear its registration among the point's + # waiters, and a wakeup goes to the first registration under that name — + # so it would keep going to a process that no longer exists. Nothing + # below needs the surviving worker to move: replay does not wait for the + # pool, and the pool's return is the restarted worker's doing. + $standby->safe_psql('postgres', + "SELECT injection_points_detach('replay-warm-before-read')"); + + $primary->wait_for_catchup($standby, 'replay'); + pass('replay continued across a warm worker that died'); + + $standby->poll_query_until('postgres', + 'SELECT count(*) = 2 FROM test_dwb_warm_worker_pids()') + or die 'the pool did not come back after losing a worker'; + pass('the pool restored its worker'); +} + +# --- the pool on its own, with kernel advice turned off ----------------- + +my $noadvice = + make_standby('noadvice_standby', 2, conf => "recovery_prefetch = off\n"); +$noadvice->start; +$primary->wait_for_catchup($noadvice, 'replay'); + +$primary->safe_psql('postgres', + "UPDATE t SET filler = repeat('n', 200) WHERE id % 7 = 0"); +$primary->wait_for_catchup($noadvice, 'replay'); + +my $na = warm_counters($noadvice); +cmp_ok($na->{published}, '>', 0, + 'blocks reach the pool with the advice prefetcher off'); +cmp_ok($na->{collected}, '>', 0, + 'replay collects the pool answers with the advice prefetcher off'); +$noadvice->stop; + +# --- promotion with requests still outstanding -------------------------- + +SKIP: +{ + skip 'injection points not supported by this build', 1 + unless $injection_points; + + # Counted against what the queue already holds: a worker left parked by + # the scenario above still holds its slot, and that one is not what this + # is about. + my $held_before = $standby->safe_psql('postgres', + 'SELECT published + claimed FROM test_dwb_warm_slot_states()'); + + $standby->safe_psql('postgres', + "SELECT injection_points_attach('replay-warm-before-read', 'wait')"); + $primary->safe_psql('postgres', + "UPDATE t SET filler = repeat('p', 200) WHERE id % 11 = 0"); + $standby->poll_query_until('postgres', + "SELECT published + claimed > $held_before FROM test_dwb_warm_slot_states()" + ) + or die 'timed out waiting for the queue to hold requests; slots: ' + . $standby->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_slot_states()') + . ' counters: ' + . $standby->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_counters()') + . ' pids: ' + . $standby->safe_psql('postgres', + 'SELECT count(*) FROM test_dwb_warm_worker_pids()') + . ' received/replayed: ' + . $standby->safe_psql( + 'postgres', + q{SELECT pg_last_wal_receive_lsn() || ' ' || pg_last_wal_replay_lsn()} + ); + pass('the queue holds requests going into promotion'); +} + +$standby->promote; +$standby->safe_psql('postgres', 'SELECT 1'); +pass('the standby promoted with warm requests outstanding'); + +cmp_ok( + $standby->safe_psql( + 'postgres', "SELECT count(*) FROM t WHERE filler LIKE 'y%'"), + '>', 0, + 'the promoted node has the replayed data'); + +$standby->stop; +$primary->stop; + +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index ceeedb6c46531..e54a14be47cd0 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -111,6 +111,28 @@ CREATE FUNCTION test_dwb_cleaner_counters( RETURNS record STRICT AS 'MODULE_PATHNAME' LANGUAGE C; +CREATE FUNCTION test_dwb_warm_counters( + OUT published bigint, OUT dropped_full bigint, OUT collected bigint, + OUT missed bigint, OUT stale bigint, OUT cancelled bigint, + OUT released bigint, OUT claimed bigint, OUT reads bigint, + OUT hits bigint, OUT failed bigint, OUT discarded bigint, + OUT vanished bigint) + RETURNS record STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_count_rel_buffers(relnumber oid) + RETURNS int STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_warm_slot_states( + OUT published int, OUT claimed int) + RETURNS record STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_warm_worker_pids(OUT worker int, OUT pid int) + RETURNS SETOF record + AS 'MODULE_PATHNAME' LANGUAGE C; + CREATE FUNCTION test_dwb_pin_block(rel regclass, blkno int) RETURNS void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index 5efb3a765b50e..53f02abb08c2e 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -22,6 +22,7 @@ #include "access/htup_details.h" #include "access/relation.h" #include "access/xact.h" +#include "access/xlogwarm.h" #include "catalog/pg_tablespace_d.h" #include "common/relpath.h" #include "fmgr.h" @@ -1140,6 +1141,129 @@ test_dwb_cleaner_counters(PG_FUNCTION_ARGS) PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); } +/* + * Counters of the replay warm pool: what the publisher handed over and + * what came back, and what the workers did with it. Errors out when the + * pool is not configured, so a test cannot mistake "off" for "idle". + */ +PG_FUNCTION_INFO_V1(test_dwb_warm_counters); +Datum +test_dwb_warm_counters(PG_FUNCTION_ARGS) +{ + TupleDesc tupdesc; + Datum values[13]; + bool nulls[13] = {0}; + XLogWarmStats stats; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + if (!XLogWarmGetStats(&stats)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("the replay warm pool is not configured"), + errhint("Set \"replay_warm_workers\" above 0."))); + + values[0] = Int64GetDatum((int64) stats.published); + values[1] = Int64GetDatum((int64) stats.dropped_full); + values[2] = Int64GetDatum((int64) stats.collected); + values[3] = Int64GetDatum((int64) stats.missed); + values[4] = Int64GetDatum((int64) stats.stale); + values[5] = Int64GetDatum((int64) stats.cancelled); + values[6] = Int64GetDatum((int64) stats.released); + values[7] = Int64GetDatum((int64) stats.claimed); + values[8] = Int64GetDatum((int64) stats.reads); + values[9] = Int64GetDatum((int64) stats.hits); + values[10] = Int64GetDatum((int64) stats.failed); + values[11] = Int64GetDatum((int64) stats.discarded); + values[12] = Int64GetDatum((int64) stats.vanished); + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + +/* + * Pids of the running warm workers. They hold no database connection, so + * pg_stat_activity cannot show them; this is how a test finds one to kill + * and how an operator sees the pool is alive. + */ +PG_FUNCTION_INFO_V1(test_dwb_warm_worker_pids); +Datum +test_dwb_warm_worker_pids(PG_FUNCTION_ARGS) +{ + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + int pids[XLOGWARM_MAX_WORKERS]; + int nworkers; + + InitMaterializedSRF(fcinfo, 0); + + nworkers = XLogWarmGetWorkerPids(pids); + for (int i = 0; i < nworkers; i++) + { + Datum values[2]; + bool nulls[2] = {0}; + + values[0] = Int32GetDatum(i); + values[1] = Int32GetDatum(pids[i]); + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + + PG_RETURN_VOID(); +} + +/* + * What the pool is doing right now: requests waiting for a worker, and + * requests a worker holds. The running totals say what has happened; this + * is what a test needs to catch a request in flight. + */ +PG_FUNCTION_INFO_V1(test_dwb_warm_slot_states); +Datum +test_dwb_warm_slot_states(PG_FUNCTION_ARGS) +{ + TupleDesc tupdesc; + Datum values[2]; + bool nulls[2] = {0}; + int published; + int claimed; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + XLogWarmGetSlotCounts(&published, &claimed); + + values[0] = Int32GetDatum(published); + values[1] = Int32GetDatum(claimed); + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + +/* + * How many buffers still hold pages of this relation file. After replay has + * dropped a relation the answer must be zero, whatever the warm pool was + * doing at the time — a page left behind for a relation that no longer exists + * is the failure this counts. + */ +PG_FUNCTION_INFO_V1(test_dwb_count_rel_buffers); +Datum +test_dwb_count_rel_buffers(PG_FUNCTION_ARGS) +{ + Oid relnumber = PG_GETARG_OID(0); + int count = 0; + + for (int i = 0; i < NBuffers; i++) + { + BufferDesc *desc = GetBufferDescriptor(i); + uint32 state = LockBufHdr(desc); + + if ((state & BM_TAG_VALID) && + desc->tag.relNumber == relnumber) + count++; + + UnlockBufHdr(desc, state); + } + + PG_RETURN_INT32(count); +} + /* * Resolve (relation, block) to the buffer currently holding it. The * transient pin is dropped before returning; the id is a hint exactly From bef8d001a1e7ece56050eb745795f10d75022002 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Wed, 5 Aug 2026 15:22:00 +0300 Subject: [PATCH 41/52] Let a warm worker remember the sizes it measured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A worker asked smgr for the relation's size on every request, and smgr answers that question from its cache only for the startup process: smgrnblocks_cached() returns nothing to anybody else, "due to lack of a shared invalidation mechanism for changes in file size". So every request measured afresh, and measuring walks the segment chain from the beginning after smgrexists() has closed it — a thousand file opens on a terabyte relation. On the primary-to-sync-standby stand that was the whole pool: twelve workers each burning a core at 273k file opens a second, 4.6 ms of CPU per request and nothing left for reading pages. The ring stayed full, three quarters of the blocks the prefetcher offered were refused for want of a slot, and replay collected almost nothing — the standby fell behind exactly as it did with no pool at all. The pool does have the invalidation mechanism smgr lacks: the drop epoch, bumped under ReplayWarmReadLock whenever a relation or a database is about to lose its files. A size measured within an epoch can therefore only be too small, never too large, so a worker keeps its own small direct-mapped table of them: the first request for a fork pays for the existence check and the measurement, a block past a remembered end pays for measuring again — the relation may simply have been extended — and everything else is a comparison. The same run then reads 58k pages a second in the workers, hands replay 98.3% of what it publishes, and takes the standby's replay lag from a median of 63 seconds to 9 milliseconds, with the startup process reading 40 thousand pages over the run instead of 43.5 million. --- src/backend/access/transam/xlogwarm.c | 90 +++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/src/backend/access/transam/xlogwarm.c b/src/backend/access/transam/xlogwarm.c index 3adeecfad70c4..817cb9958e6bc 100644 --- a/src/backend/access/transam/xlogwarm.c +++ b/src/backend/access/transam/xlogwarm.c @@ -160,6 +160,34 @@ static int publish_hand = 0; */ static uint64 my_drop_epoch = 0; +/* + * Worker-private: relation sizes this worker has measured. + * + * smgr keeps such a cache too, but hands it out only to the startup process + * (smgrnblocks_cached(), "due to lack of a shared invalidation mechanism for + * changes in file size"). This pool has that mechanism — the drop epoch — + * so it can keep its own answers, and it has to: measuring walks the segment + * chain, which on a terabyte relation is a thousand file opens, and paying + * that per request leaves a worker doing nothing else. + * + * A remembered size is only ever too small, never too large: within an epoch + * no relation lost blocks, so the entry is trusted for "the block is inside + * the relation" and re-measured for anything else. Direct-mapped and small + * on purpose — replay works through a handful of relations at a time. + */ +#define XLOGWARM_SIZES 16 + +typedef struct XLogWarmSize +{ + RelFileLocator rlocator; + ForkNumber forknum; + BlockNumber nblocks; + uint64 epoch; + bool valid; +} XLogWarmSize; + +static XLogWarmSize my_sizes[XLOGWARM_SIZES]; + /* * Worker-private: the slot this worker holds, or -1. Read on the way out to * hand the slot back, so a worker that is signalled away does not take a slot @@ -504,6 +532,7 @@ XLogWarmDoOne(XLogWarmSlot * slot, uint64 request_id, RelFileLocator rlocator, ForkNumber forknum, BlockNumber blkno) { SMgrRelation smgr; + XLogWarmSize *size; Buffer buffer = InvalidBuffer; uint32 expected; bool failed = false; @@ -529,27 +558,62 @@ XLogWarmDoOne(XLogWarmSlot * slot, uint64 request_id, if (XLogWarmQueue->drop_epoch != my_drop_epoch) { smgrreleaseall(); + memset(my_sizes, 0, sizeof(my_sizes)); my_drop_epoch = XLogWarmQueue->drop_epoch; } smgr = smgropen(rlocator, INVALID_PROC_NUMBER); /* - * Re-check what the prefetcher checked when it published: the - * relation may have been dropped or truncated since. Both answers - * come from smgr's cache, so this is cheap. + * Is the block still there? Replay may have dropped or truncated the + * relation between publication and now — the ordinary outcome of + * running ahead of it, and the outcome the interlock guarantees for a + * request that gets here after a drop. + * + * Asking outright costs more than it looks: smgrexists() closes the + * fork first (mdexists() skips that only in the startup process) and + * smgrnblocks() then walks the segment chain from the beginning, so + * on a terabyte relation one question is a thousand file opens. The + * answer is therefore remembered per epoch, and only the first + * request for a fork, or one that lands past a remembered end, pays + * for asking again. */ - if (!smgrexists(smgr, forknum) || - blkno >= smgrnblocks(smgr, forknum)) + size = &my_sizes[rlocator.relNumber % XLOGWARM_SIZES]; + + if (!size->valid || size->epoch != my_drop_epoch || + size->forknum != forknum || + !RelFileLocatorEquals(size->rlocator, rlocator)) { - /* - * Replay dropped or truncated the relation between publication - * and now — the ordinary outcome of running ahead of it, and - * the outcome the interlock guarantees for a read that starts - * after a drop. - */ - failed = true; - pg_atomic_fetch_add_u64(&XLogWarmQueue->vanished, 1); + if (!smgrexists(smgr, forknum)) + { + failed = true; + pg_atomic_fetch_add_u64(&XLogWarmQueue->vanished, 1); + } + else + { + size->rlocator = rlocator; + size->forknum = forknum; + size->nblocks = smgrnblocks(smgr, forknum); + size->epoch = my_drop_epoch; + size->valid = true; + } + } + + if (!failed && blkno >= size->nblocks) + { + /* the remembered size may simply predate an extension */ + size->nblocks = smgrnblocks(smgr, forknum); + + if (blkno >= size->nblocks) + { + failed = true; + pg_atomic_fetch_add_u64(&XLogWarmQueue->vanished, 1); + } + } + + if (failed) + { + /* the block is gone; there is nothing to warm */ } else if (BufferIsValid(buffer = LookupSharedBuffer(smgr, forknum, blkno))) { From b520f8b1d606a5cee8dbee68af4c2ddf60f22570 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Wed, 5 Aug 2026 16:19:19 +0300 Subject: [PATCH 42/52] Add the over-time charts for the warm-pool replication point TPS and average latency over the run at 750 connections, vanilla against DWB with the replay warm pool, both against a synchronous standby. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013tqXDnyVLb628yQ14HaznA --- bench/README.md | 4 ++++ bench/repl-time-lat.png | Bin 0 -> 55959 bytes bench/repl-time-tps.png | Bin 0 -> 60619 bytes 3 files changed, 4 insertions(+) create mode 100644 bench/repl-time-lat.png create mode 100644 bench/repl-time-tps.png diff --git a/bench/README.md b/bench/README.md index cf739c44eb70e..9f199a6152d5f 100644 --- a/bench/README.md +++ b/bench/README.md @@ -14,3 +14,7 @@ and data checksums enabled, converted from the same reference cluster. series: throughput and shipped WAL per client count, and the standby's replay backlog over the run. Both instances share the stand, split by socket, with a netem-emulated 10 GbE hop between them. +* `repl-time-tps.png`, `repl-time-lat.png` — TPS and average latency over + the run at 750 connections against a synchronous standby, vanilla + against DWB with the replay warm pool (`replay_warm_workers = 12`). + The vanilla run is 900 s, the pool run 600 s. diff --git a/bench/repl-time-lat.png b/bench/repl-time-lat.png new file mode 100644 index 0000000000000000000000000000000000000000..3011750d205f0b1feeab168707104916ab4f36ff GIT binary patch literal 55959 zcmaI;2RzpA{s)fV6v{4Jwk8Qh_MRajn^4Ni&fbzr#!W_KB_tvwD|=T+cJ>O%&ffpm z)%bkR@4wFDbWU-P_kF#u*X#9M*Bzv$sz5?WPl&-_NUkW#YG5#j;1ABjBlz$W2kJZ; z_&;GMIbEmgc4khlHy!U_uHJOAx3+V#w!FpSa>wzWrJe1i3%tA+c+ay~I62wh6Xxc& z`R@@I>>SOx9s3V$!mAv$SJbu+Ify==ncmB2Rj~9xISFMh) zrfu9FNEl&`jGHrajT~yV{<-7M#B{mfQh~Xvx$AV|O45dX2rs_$5!MIf@INlsHJuLq z{d-zgisTdYL--AYtUu+SM?4UGjC=0y!yU-+G=4wr0r@x!VcOs4)k{$-9{cl%#{p-< z+5SF)K%GVI?;}VxwBTKE{7jp|#u`IUxXq+Ld_CKm8o&k*1iQ=RB+363d&avLk3eit*W7 z({ZPl#eDO*+M8>P#fopg#?}8WOmo4oI^VRZW#4;q>`^SwP269G~jiV50QX!$J2jXY*f zB?vn!t*`6g@z1!iMc1WWiOjpzKo~E^(;cLmQ8!>$6?#p4>oB2QyR+l9Yu9!+C$L9| ziDm8W^IGRKR1*nP+aDL0x7F6w9a>&qw&>2His3SN=)N))-E{qR%&^zW)m6B{OJ*&S zii(PI0*^k1;GWYsl_2PVJN+f`5X`Y;0WM{cQEQyu3V{fI#n0@oPzXdDP z!-PC-sC+imi~yr}W9-wo}{p#mx$87`y^uo zSA9xKOUGbEzgiXk=*%Fq>dp$Bo3oywof|AIXtmMeh;Hp2j@jMyVi5PD4GIc+{P?kC z@7o)#!vP&h2CzSm9XsaFUAc*CSGi@DiB-s=l~+8)=~%=6fh0R@9NU*DRZ@(T3l>rT1KXCrZXdbPhxMiGaKqATd%;(8v3L)G*Oit?E+}fBiW3fK7Hz_guC&X$#>U* znvt>9cDVeLYJy-5CXeEbYOGCxSu6e2!onAg>(_0>t4c})dtS;BQ8O`pN%CIT>v?Z^ zT|3uE%w>H}uaVXiCcrJsx6*slEk(hhp?9nbpPxE)N@&-Rot?e!g;c<|&E<*o`K!^Z z&%@|0JzSd~=(SG1`ke3P*x1kTx&R~w17=N-_-1EbCSo; zbC%wBhR#i!e`0SSSnMM`m!Ysu1+uPq+G#j{@ES~7R zFsQdJ!p|QqCnuMG`s(vMXbcf+Kf8m)etl%`xL5S*z0E6?xY{VCQ?Tvgom#})mi*w} zm%Fc|-3h=axs{ZZWL$AfZA}20e)54%-nuRuZccikSVc%nY z?$giiH%&?IQ*_i+R1LU?4)u-r9&qKXz?3ME52fc#ZMP`0sq)$T2K~dPk~)MJEy88% zZ#?lE!Ha=~i^)(+ycp^>8<;u77-eN;(~#0B?^`YV8>uiz&$F@J4`mQ~%5OKEu(h*O zz<;+V51)iSZ>;W7L2NATt9FZTWD>h3`8JlhVx}SP?(X?oZ?v!YG7CFB?tXhCp5L9c4V8W}O&g+X9YVxRJU4sPmg zm{$)PK3tR7<0B#@yydzu7!h+dK`;WQnW`|3&-P44Tzq_d-<7e?(VTJmIc7paLLKh_ z61@BPQE{dzmbY<#Yns_*Z`&C*FTjIa6OEylJa?ffyyG=(jN0`2T^H`(kHthsH_)1@ zyy<>d>Amv~ey=3)8tvT9jB5e|Z($_PWo9HfZk9SSG=(t;Y21pZE!camPm~6IsRg$J zCNvMm`qR@>9q*ETwu5j?1Z)TGU$;(jojLOeEr*Vgd&77oEDA3(GxKGWY7AO>YI5>w zVdtMG84rc8QVCBGS;>yuAy zkMbD1&)bL!kCG5i#CN~EF4sRdH@DAx`}?Ozl}t^2lC+;aIn7MsULR>qTP*nAWh8k# zfj!R~P*Y87I`<_}L?uNnFWGlruYUoqBxFKxeF7^Hv&jJct97_;T!}CZO0fl#H&ARB z&oxqd@64g?hAf61(~oK?x*RtS*$@_^~* z2+6F?*eKoS4y^(5v|yZAlxSlG+}L zHAN=0al_z&*8PCw~KTN!vo{VYk;UPhE3?o%RlV<1)Dt&fSQu; zArmw``-F*TF5HGixMyL!j;~=*WPQ`f$jJI&f6v8OGT%J@DQ+fot9Jkw@}X$o!A!ir zwY~igdjFM;fff4t#>R`$N_TqV;idtEcu%j`nk+G5-3RyVPJ|85?T_RICN_Y@vVHcp zsVg*Jt$KK8HeJ2q-=$;h-GGU`arS0NKD;(BX09V$IfS>j;#!E%h7U0(~nZru8A>aF7wY@ofW%fP16*k^I+Lk zlEoR@+uH#L9}^W5i%3Wy4wb(??&9F!0LX`oS)7lPw3;>mAQvo5nwL)}hXN+en}(7S zL+&_AXKK-r7S!5=W(1G<7Y6IWl4H9h6o#*^oJ_B zU~1Y+X2ioDvs?%yyFpvahkFY^ivVVi>t<V+me`L8sCZ=v2akX4e6} z3%bn3*4fN;zj>zFSbp+^fURsWH5XzedV?Du*|D*EHvn<8Jm)8ZVIS4#Z^=jR=HbD{ z&R%Cdl#swcON@UQAHUyg4mBGr&5Ko2ea(I}IXR-Ux?pd7_wpGY9{vVHajGpDo38xq zB~;^&ktzuoUAPhw5@iFo+kEEs=9-tqOxt{98zg4olS!GFyl7a#K7L%?YwUaDr)liM zNQ8IvRG)cr1C;La?n;}oX@qIVsrpEN2 zn=TY^p8moJPhb#oJPchwI%ed9GZEbOuQN_ARZLce^nN_+k(%6;iV;d}COjrUGjz+{ zcwb#hzQE-RG}#{J`|)C`Dkr;#qlO)U#AjcXUiRjsRFxD!`#%@o8}d8 zD`{}nu0cQ&O{bUN=g>sn_1uafRCTQ@qrRp&_%!lOSMGYQ6(eQ)spXR2B_mN+tgla z6J_TbDRE$jImvm$o50UuqH(NrI;jrEp~K>^SjY*1FQ?kHZqM^aM{@QXYC{n}fXjca zo|--EyE1jP;3z+xDH1V`(bGSH?V!}0?71M+k}Sat#G`|34PNFLHMN;~M01o_Q(Q$; zu;HA0G9Ufb%=w~eH^=WQYR})l=i_NqgGO)|4{vNHBN=EXzLEEu){-b$ptL{m&Rr-< zYOP6zA+pg;^AUQ_D)WTyH#*$*43A)D(Zf{2W6?Y{Q0ZM(N5M8wkn87%0X9AQYqD9R zZ|DAf+_`~bGASvkj!NJbL7+)UXKKC#GGft_O^1{f;Ow+0%S zK=A`q5WQ^k#k(&$XY%e%tA^0>;!3tJM$T;bTL31Gy<|?R$uYJ%`zE(iT2b-H%a^HP%4scTJdLyt;C|YkboHDy zNLvBT$%Kb?3Ffb&@xzx3LuID0o!t)hHsG#sL~9Sfx1>O2YgpxTgF_kmm>!Z#GN3c? zi+$C!wmuJ}?oktyCoy6bgoK27cY7}YQIis`$uzDK$0uV7S*$u>x}KpL`0SZZ=FSa} zjkee4gP~SgPoKs=qnc0+U?ksr$E6@D0st->3kwDwEQe(h`&!~)TCn$V%MfQEH#6b8 zZ2`kP9iK&bE5$RI8cLJ7bf(`5@6x1f8%=lexhY~LQWNCKziZ8@Whq3)6bqie{v?t4Htl;oTet_yLa#MJl3oL`(U&0;#mz5?dz(zZ(eb+0zTHW$W10Ev_UsGpt$b^P5vGafnL- z)%AyBgmyYSwMugC+&M(&le~UiG5p|kG(SK8TZuzzgDI9#gc`W|Y4m6qbN$^hs7uat zq?^@z?40%j^J}8OZ2%h*6xj|D7V3xM5m777 zmW+EpUD-PuRFPZp8me8l)bSL$#S!cZFM+6|&bz<21AUVS=2YWCuPMMK(A2`S+a76> zqTw=@-^3)Nsv6eqz&%vv+JqDiK&pl%_BcQbb*p^Dpl`OCWityop5&t^+238c1}jY9 z{Ig368qU{uca%Yc!~@+f@-#in2OhY;+@XEYE%Ft`mRDBlfTy>r8oo?Tb(s82g>)*N zw}w){k5Cl``TQ<$y2PXI3NWzLPxf~ zmuT#>g;%xvi-?q2oVdb>d+j)NGFCRV-2|568`Kg~ zCP3>ztBix6OF=n$LYNKb)(+qkADjn21;F@2mCJO7myB);G{8B8p z2`$L#4|TMge-rw!PjP-qM{rnH874tJ1=WOVh*DbdC`>8>P=d2{^QB|<+Rc+cvT|{e zpgITej%4k!V$YRU0aR>&Be*YE-{`J$V5r>vr5t>vpt%n4aKzK6_j3%2V|Gj_dQ+Ls zN#A~N+C<7A;zIm;CozfYwzslAhD*rCQ@b?>ti7^7PY(hFS;LbmZSi88T9IE2SZT2|F}CELA;IOiHV^foM)e3wY9lgNSb#1 z_;GS3CZqONmOt)|CM(zvGP=6DL9?2(lAocS`YlPvercH}6m3&pxg14Zahy#+cwbR2 z)ihl5B>KzCiCm9QpFc~xxs^~VM5XByXxtJ`&ds$eJ*mTta!-@1Pc1|;wMc*gl7W}zt+!VBExwI&B)S{9~}7JM0~mBX0V*E7O8cV7N6#?tP?L%vT= zrhvK%AV2%Z6!{r77OgcmH#ZFC>C>m=$B&OYJ!ihL{(BWOS>#iXVMeE>v}?Fn)69-> z8^X0`Y)Jia))etllZd51KVwK1B24=n#LC5lr$bg7PHo(N_4i_AzIPI41UeAlxv`+P zH=u`b-*`jtq{U0?cLfFQudbf3ELYX)bG`HI*|SG5dD%EQ!Sj*v*A)6FRDu(fD9QQxowt`^Ed;Og5s>F!oWGsvigy3S*TOE1nL*aJ`%1R?; zjw8{D85w85YzhP^?rS>sdWwG+6qdu>H(pd8=3f%dISj#!9Rn`MLrXl_CG1YyF&?oz z$>+y=6K5i#(vp5n<+m*;8QXd0@?~l;^e%XYKbR3jn1Xfp;6yD{5PGR|3;VHiDv(5^mcHm=|H) zYsV=l+M$Hf($ZLjgy;Y`P7Zv%$a8Y&ux`FPs~bD;)!Uz?2ytP9D4iD&&(>$}uKjAV7yH zE(?7H3%dM8WN|d;Oj0#Sba}3eMg=?I^;oSH;yhRu2gyYiPju@vwT@$a_P5MuybQgr!QzEJGx9i!{4f}^ z;jp==IJIBsl8@OTStrg)A(P!&D1hOS}LXLd7u`Zae|KtgpfjQ{sCdj=&};p{D!0TNpp+ zR1=A=!!;m!rgt?UUla49+sa{KP^e%-6s8#5yU2df{D1Q@c7?EdWPiejeTil+_-#Ox z#gHuy&`}2HS0+{`LHmsyFm-ZfW-JImDZU5$ZexKoO8{hI#VT`5l{7O{8A18z9ouLz ziuRBmhR!lJUp&%?w$LLo=9mwa+Hd%G$e%K?l z)L8CIjjp{_=nZV{TV|4j2xVo%Y3MkOFn%4DMx~G`iwHCP$o|*&E19k8>gpZa03K`G zQx)=o)gTR^Ll}hcb6`vy#l(uZUZCdEzr9`_Tt!;6e_?+43Fq0f2| z0PbmBIn@*64Wb)D@fZ`RWQ2Z~L9fEWAV3E010@hSdDtz=UghJJd+P?b&CJGuhu)V@ zl>!D94puVM?I$6YG z7VXmg>pP28P0;T`cQzJf-gJ*cNB99&X5Wg{*BAay0|Nsm;^N}WHiCUdnuxm`Whr8F zR*)P9yu@PY1HZ7-4?o0C>mMJZ&Mlo;p~BKQxI@ov?H@v2)5go&J0d0qKcGf!e1gh# zzMmgVd_tvkjSp4O?KhX|$mXFt0n7CM1w8#_P07NG`?BF<|u4jYz2_Z2fF zDnflW0y+lSP76~FYaQwN zX$buMu7-`;EICCdV9S6^S%Aa12rqzboj$+5ID$olIu$D}y)xAb)~P?-HAdmYFLUm3 z!`d9t)Xnt+>3cW&C#W(sGRYYkLjlRZ04cz10}Ky+MiIOtrght^GXa1FQj97*s277Y zGWk43J0l&g!%TPETuNyzieBx1@ZbSBw)miw6n^VzZf<_-?YlMkELPBgjS2iPmFIj{ zK8%cvFvE4-Z?~xLD=^PDsli3D6C^~zRpCe;dEc-H=*oXWFn%#pIwgFecwIPp)vW=K zfPnqfsRnD~%*;%52WDfB-zaxG1{zBK*7jLS!mcbWqJ@Qp4#&=cbW|B_E07AQhdzKK z$sx2g6>oP7G6~wj*DUzxE%;)L83+Sk8dmBGg%xb+8s5ZG68Cd#oVu3v@M)NY^yfp2 z0cmVn-4oYDw0VVEjNyFDD$9g5}{qp*`CG#PMm zs6mhOSo?`MZPmfP2z1DYF#9t3%EcBl3wtxunmBE%{cuoyR_-i_5HpK2fI+ai+IfxF z>4#bwvjjgs6(ah;uye#8me?#Wk|c zp3vjbRe++EsWI6mcZ;QA_3c&OlL+d!j&*yV=gYbEz*0+^%}~F^YVKS9W}EMR0HA`} zkr88k@muvrvh@oIkuV`CiP7Ysf?YZJ&P8y=4P*fYaL54Ca<3JAoPo+mY`c4>wzf78 zIGsu?j}-m=iy-QY9_+4|-Mik`?*iO8me2AyD4_VDAM!LV0gP))@h-ORI}YGmvv5vliGe|XDy{ie>j0OT5 zAhConOE5v*OFKGVM1ma*=Ds~QL?FSl49nx`^*$|VX0xg#q*vy`)${ho-6ECL_EEUc z)U!})RIqdnAea&&XJkD&IvNlD0;3{E*~%CqG1Ah~+4AqsB@-*u1RAHd}3m3W+n*4GtAkwLf*Bd zF&T?O{jOZ&kr{6Y2eE=uK3C|qxx@2ngHaNhPBLI5IJ}5 zC=5juD|^2OI_hsG-kT5m^$Q3c0Op}hTpUoa%#8<3_C`AgZFLaOdwy2hc&{@AF~b&h zEc{HVnC`PL`sxv&~$^+6?Nm*XUrLqWw@vq2beQq+=0y-$ndlr&|QO;HH zmWnDWkZF5x!*t;BtYxA8AhviLr*dHw*^Vr#Njx!oo%%*-jHW1%e8REuhXp}%GOPwh$@zhkx4|wG&D7t_Px8K``+RhNEB(_hx{NX z_ngo2J}P%KLW&YWt%IVjgD!f<{q2Kq!5+YsbqmQ}bcv%y)8qp0TfxQUGCiLK2Eu2% z>jQQceeX`af-qpxG!6zBNyEYZhD5=<_~z&#G!_9$JdJ^P(={M>6@ZWS2vpqd?jP9kpu3!Hh>?PzU z55EFCb^-~ksq&!+Cf3*2k3xCNL-Q>!FJFRf4#MFW2>tZ{lG?MhI5E}eFU5Ao@jFSv z&K$V7xb{E3vJx{0s`X4G+60{ZCd{nOwO(TffD+|vJq8Gc0>iWi;S|}pZ4w8f@HEU3 zF{7%z^%F@x+m>En9Y7aC7RJ|d_j_-!4p8)7fI=}CNC%><98@MQ=7*Co!XhXJ30lqj zR(Me$I01lK^yd~qnW_cQ+MafW0@@+Wj8q^Aw8BnjF(xogjt~*m!RIm>H;os6hGKRS z`Qe*p-|jmNR*ek;fi3O{-lIY>z9Mr{_}3Kv6b`mM@W;?}KO)(Zj> z63n7)b&%d&zkIh)AFa`kfnsi?5Cg?#gWn?d0gA)u!a^8Wrl}w~Ai05M)s0K<-C>!B z#8PHvr)>vI{DFCo!g^^lLutQ&-EQ$Mj}=laMxOKhfIkS3`t$H^@7tT*Sy~<2AP2$L zf1q@y!gm6GtAQN=6ogno+}du%-ugfXL;5MPqM3Vi;$DJ~`%s6hEb?OEPE{M&Rp5n9 zG5l+yu-E7i2FcH`jT2YtfnHNWcY+OBf0mL--11Bo!i=F*pKz(bRS35m26K%puLg2#Z0xPc4B3%N{8 z?YBahle+V~6m-P+48l%Fz-YnvA<+Ync&+cjj&V8U4N(SMeESDGrW$09cF1}IP(BJ& zi)(HF^Ai{dwVC{KzG~RtVSH;r=G=xc--N0kR`iCY&s8%5G%}<1!$6w%xVFA96b!V7 z1-v8xBZ3a&hmnVaMm9ht-hpdichX<(jv;hDLXF!NF1G$f@%=e(OE1pd}iB=Xz5-?w@NAwdBcqjDUkW~cC z0Ly?TBE-IA;P+#blY#J2#!yZJ@(H}K|4Pcr42E=6REVcq;~#;Y3h{L2*vmHb$mRvS zI}Sho(UBfZSsu0i4Fi#aLj}Er7-5&WBZ1P2nna|e59L#9VEH(Pm;oNFd+2tiDv%Yp ztS*i)0}CZEMY3cKC^~67@L?ZAsAwDp2fUzi(8hHZx!1cwXgbby2&-J*R?3@;IPPrq zV?hn8sh1$?ejILA)cyjr0hF3SK1yF-pWF8c1q=q-MWyq80ugc@ZOynYu5MW`G zncgypnOBN|_CJ9Oh+nIZYUHy}KTC;D7UQ4(k- z7w~=RU@F_ZM-zu%R}>R86HxTcBNWE3Qu;fOD*c=buz^Bg1Ko=0SNPLFZw0Vty5uWd z;Vn9F}cq%%c^0IYZu(5QYu1}KW77mE74K@qWe zTT(+6c&{28iShBA=`Tz50UslI2ILSqVTZPsmU_^K)18?_-TXkVdI2wo@dF1GA1k&V z+r763EE?VEVV@lvOf78&KqR8g%F(+pttae;B#4YhBkz1xdlhd0%P=bp+}0amXud!u`0hZm0tAfd~RVa)3|2 zLarKa>KMf4HP%EV6-ZxS%5bzr%%g2H_zgON=NYttKJWAxC1Rk zt4A4VMn0Qk1gNF}Vunw>BP1q{>N$G!C<+JV`yTiJ0zofv^5kI*k{bZ*T$xpcwg-x6 z6KE!*@TPDB$05oj1rk?lKdAcw5J5!IIY>|6Up9vD&WCI%SbXnoUKXc{EF# z%|S~?w-l@K8<2sK(GS!^A;DnGP~)#PNzq-hPQw}vCexi^g7o!hcGXjwhf|KaBZCk3 zNP4kQm<$kMziV2qLn$`30XMfn*aJ5aTyG zYI5GVa&XQk=}++CYl;ja93beCQ&3z#b~PF=ef>9WB-aqrw7_bDa)S6=EvUSJl~Nn_ zP_PgJKM$cLv-0qeBWFc{*;^BbE1KZ%u7dS>{go*b`Wg=A;>C+g;GpRa9{3(?_`;#5 z==~lGKHXwlM#%Zyu(gSz_@B4?$*b{7it=|B^Zfbj`})^iUQzr4zWayiYUu>7EB2X! zTk`FLGbd7mUJ64o)B#V2B{PL6AQCZQ1`y?docy z{hGqi3*zFAHqufO&N6zRzohUXG5UE(LfXk^ML}H*>kPgRN{%C8v3*&A<2a*lx3Y^% z(a@~+wqzE#Lx`|qQUv9a#l2Cy7fRG1Tf}Zy6f(BT+ajEtM1aSR0Q7;ZoASV?6;ZGm z(1|y)h;|wOM}fuOqFqabM;$EV6DL z8tU)oP_Siv{UH{M?Y4ou4=C#mj)BtZ7YHslIh>M+X)O3rB&E!JneB}R78WPue;21N zVk1xbP$r-e6i6=t1@dWRB(e(OeACU&S69DU@NND`#iFxGpp=XP&@b`axBw>=QF*=V zQ@zCLtNI6!Km&OPN{rQTxe$s^72LTMe)0>KGsy3TAbkOtM|>PuM)q8$)o2ulWF~G1 z{=ZYFnYBn}eDVZ*0nMOUbXXH^4`&8-DN}|c!KEp`nSYsxw5*r1UmQ=r+ED)kqK~j@ zq5cwbFZJ(X*YK<`C>43Jzb9KYy`iT6r38*-ZbRAH{X5^PF)2i)2&xZ4tsNsU?HiQ2 zD;@xVk<94))Kl{FvB%#tv6>>av+}<~ zwpuaQe+SS5A!mYv@!6Rd;Ir&FjMz1r;gC7J+Jmnx0LLzWw+Y4WX-$@B2tC5#4@@-- zd|t3^F(`=~{`@(I@oX0Bx$u{NdM;XGxD;SVdDeXwQGgI&nH+#991NU0kk!j zz~3$2=^D1E)%c_zCeeUs|(|?68c~v|Oz_&s^2Yap%Z!%a|*Ys_`JqOX8KdTu% zq1i1C`5X{JJ`csv*SuFCvJ0dv_;z&?fU(i^h(HhS8r7lXWsfV+V0J zWEPEht&jkns&Po@+k-GuOu9q2`ZDj|Jr$nS1TQmO?*6g06%Uj4>ec7|7e7@|n~(MX zcepx>Ed%#=Q30w(6lichbN#<=4flmt1Cl|H!@^?9xcG7Z>yB%%A>mvC0eEKN=okOa zXKQ+^rmv%<6c{&~;DDe=XYefwu)-bR=ji{9vpo2Hzfrgc4pFdSsxA1?b%p(RMdENp z;9@H+xOCC}R|0E7{PIr(v04Ad!5WN};r})e2Nr6{te+k4zdC&i1!SHejq`dC^Owd# z1x9E4cLDBESCg!fvaB~c_ur$R-zG_e6d@wQ)y4cM?qv7ryDU@`6o)Xts_!S1<+%LU1g!3H6y^fDh4aT;P>z$f*Fq)#E57*a z{+iIrmXi;5QR1-%n7G5{(#_HVt(+_Vlvh70|J^6wHCficq-F!Z5uFHtk=G7^KR%m& zN;o4bFD-q4&?ejEzop1(oyqcs(*SN$app@9ietHOffR{d$ms-3YKnGMnQLyse6}`{ zfawj4_b47fm|{;Qt?x;UBfIk^`E#hRKLH}LE7 zLG|Rb_*#7{5ncyHOYhj4IKwfPKjoX*56uKl9tT0wkx4mQDl-fG`qXeTS6n?OupoepxB6i%(sGAZB6OQrV<#nHDd9 zgNc%mnWiYD>o6%Jzy-|z!i8Y=#Z=g%a848sD4zN4Up1q!4fu7!1ul_k&AsM^UFa$liSq8ZzpF-v~tLw6@_a0 zh9QiE`~UdRWo2bUv%*x06<{!-oFp15Fl#XwIOm7%GdOxRaX0kSJ7>@d2^Mj2CQwRb zBqb$-X73`Wmp*tBIlZLTVH0y6=hGWgEl#9{i95+suQS1W5&rLcJz`%>L4!#p{xS&) z!9!*eP!>$SYKH`wosZzy!+itLYQjfd5{R_nFqlqn-A&B;Xbxhmq?z&wq-hvMDQp3_eZeR~)G-3z4sjOFA%c`d zQs2~sBm#^doP)JYgbEmgZ)*4=@Zlm4QQ9xgyg!(?TL)rKX?6*?Joxs6hePymau*;X zrI+^Ho+JEoYik3H?fal(uyJvzgh;$baDDW__p zq>pMT$~N#n(d1N(re0oMmDQz{`}O4__hqTSz0L7D-ou)mAiC4af`>zlR4deFFT<-t zuBQPS=%lT3Xie$A>v&`N+$F-cte8?_*#c=FX!B?t+N?y z-W0(IfBlC%JXX%zwT_esq3R{q;DSMlzKZ}%n!^~{P$xCvCx5A%A-1h8%C_Ks0-^QM z%rIDw%I%+=r30Ygb^l>#y%Q@1Pi$#!Kg&DK8g|brgz_AFbi7mKzo!Sp9o7Kk_I*l| zrC~)>eCs=ftb#&abF;F(IG}wnh7d}6G9mbar{W*6HVkOvRGNo4GMfO5VAvf;;9w*j zNa7DcHM4Yga^eGQdD37a>(aUZdrmE1A=dI9XWpqaVMmT|{Q@%*=->|_T>P918x&Ln zQu`T&vMjNDN9Mm1ZJZtqv7?Q;ctsC21z=rK;upM9Om+5Z>7z#k?+P*px|kULLX8qi zJMH}hg3if4qd0N`SS)_9WY1hnI&8wEXfsmD2qtKF#+~j2!E@Yy`JMww=Pe2V6$z9P z5!WZ%Kt2owbUOx;#~}=A7vLk-fPR(_-hkc--5Xajy*2-k)A~~?(-F9sN)`g8Dncvb zOc%5VRbnylEg5KNz=26#US8mgXq&TL?NV0yCxpChxQ?ZCV1*%rzEI^;{8soy* zECeRyeHWiAKl%76lj)oROqj8W3HIp=Z@B($Ylp<~iB!zb2)192uNADme~i$!JA{FJ zs?*GII9SGZtvO5R{~l@SjagGX<}O63=hUX3{5Dk|l!Wy$ zXaoYH`1mPB(h9UTj2{FN&+EObKSh$o`xhP7--(Ux5{EZ-bcDT!r+(**>PMnOe{b8z z9JiT!ejaI$%M-Ht2{2%G2zsoZ9UmWulSeHJ@t}~zozNbNd$rEn2nlh#;>SomNZaP& zyj8Ag0Z! z1YbStB~xuKe)yW0hO9gwQ6!Fnl!rtf3~JPH01|~IK;L4&d*}^kz7?}%^dC*>d*d|K zx}pQ&S#KE}blur-^Hr0@L*Wu|F`#aPQUrcQ<>ojMq#%KAA zYA7G4`0Blt2zfPpRl3l$yYcAR#= z+OCIVykEhZKq(xszoT*-y5sb$nEzcSr`gl~4Eqgy$rZE-v#^?mi*NRo_(n0L}8GrvFWpFhz3QTI;ZY0iwercDHN06@@3(NP)(WZ2MEbz&|gng z`_mjhS3?$^QWgjEJIW22oY&@ra+%&}kqAB0Fe#%CbQ`eY@A5>#AztEmN5qN&%KgPH zir*|RTzp?co&3~zD2%?WWymVFoGfg+_A>&P@XZ!{E!1rjSC&GEHR%+kHm3X`N#}yg ztBUn5Xr4_W3_5$zsCjuCx7q^3LLGnhXT&&>uzny=aw(&$nr7iB}ARk7uI<_r^@b#HN%Pt+0 zW)7Mdoi6q}!dOa4*k$6Bidy6a|4EM`w->i)``-}YV3LNKstfAu?Tv6S7;%9Lws`H# z*bT)k4QNtF3DY=^WM~{By!o2_PpPKyeK0r^zBW{b!Dv}fY+?-wF=~qbn)-o-Iq!H$ z(x3^HJOPN(wz3Jj8_4D)zlk=)tU;bS1EWABU+3?2YaADsIFV^weqQtC^{iup4gr)5 zXUlN|onI>`)wVpj8jFp%1@q|kIItO{|H0u?bXt1XSR67Kvpolr(57 zPlmql2xA}EE`~u_Es=r!v%X-Il-Q5qVi1V}nX*V`L&q7)ONk=+ZBtk4mC?8*fi^!& zgQw)39TYclem=2bfOhiiB79iy2~EHJmv@4CZ?-7%#B(aC%pbytb01Ple&J|iey5(W zNzq|Je3Y;$?cTz$30@ys*$GXoCL}>nm>DzW-vt?CbTF@MgW~w{`vBPye+0kVb#tEs z2EO7s$djbADBb??gX1mmzq5f(W@v1 z@?$C$>S1N|!0 zqHO`ZQ(q?_nVy$(VPtZiU8^_JrhMsVubgi&>AeC$qXeP-Bl(Yb=fy8pdXY$uQ8_fC z?NCzqgXoTln5CFO(~I?O^|U5c-VvWpzTo+z=YoIkGQK9ZJv&6YHHxb#MRGpr%=Flq zq03WgTFt#R>ac#(V+N}pl^YmwO!H^rCKgtvetg zLk0*1Oy1iL^~Vi~*nrptvC){{gcrV$?HGLjg0b<)@uKr0puoddas>p;?~|vy@4UqL zJ)uMAraIOmvSxk!sU_7%S9y#B!oy7pod2x9dN&SP&XB_*UkK-;+KN__O=9;$ON1EZ z(!+}cGanCT@BXlh=6S1?B5U8du7=;S!4#q3yicmqZ7xM5Nu|~_Rf>BFe_U$J=2{?A zrIV_^?2QX<7$%&WC2lXVi@W#32_#e6BG<~_2s8WgNBrZQC~^#EmQauZzRLp~Quw-> zAmGC$0PNrkGH4NuM=1*U4#MI~WMh=@MFOGnrul{`YEjMptw0*F>GmramKJt7X3<;9 z9!c)lt{lpM?umwWh;)0Ny?xpw2FmN}}eE+yluhlz|d)o24m=iEV;LuM`DKEhZ+X3q>5D6TBxHG`F zje&aAh%8Z1QaY!6;9L9xz~n|z14U)!I*4^bCKLr-%NBBfT1Xq+!oDB8P!@%FsY{qj^}y%=-+(QfmLUIcA{9l4jCT-y}=qYi#dYFgYUCs>M; zPR{En{?0@hsDlaFiqkA43mnl4oYtP2YzySo_)6nPM=~09hiOdON-!hr1so7ZwhMJL zFpUt96>u=CJh`1bXpldzn3ak+$E^+-s4iBgA0{f5|Uf-z7=SGTg;*6KaK|ThxE(QLW zHFF!4A4ya_lS2WSew%xrAg=NR30yNfZ-m26e?R4-$Vsh8=Qe8eoatDNE$RFj@f+Cq zD;}~BUuSv==buPkQ+r+~KVzq7sMwi(qWWBmXZn-Yd#OuDW2S0(&GZ=^%X`EGA1gR`40zf-DvI4*kW&I%V4R9TjCA9?1UVi^C zvfeT*>Mm;g9$G@WK|-WKI;9oqa7gJ^5RsJbmQLv$z@b|}xi2}GP?G=3UKII7`M$`)08)|RW)njGxWp}$8!K47pjb`Z1<P z2?#h)C4>@OK7XPmu%O^zC5b(kl~;gCeOXg%d1^#>^eh&rO;y>y|8~CI>R}4vfto0@ z1R4c~7mOlFtys6neL-e@iczGr3{-$sU{MT8hAaF<9mddUuOD z(7>g*(8T^sG$iIYfg150|3UPsu*ua4{REZl?o)Iq)>15Q5|(xfDm0SEo?0vO8H^tq zlL&nr{B9taKpC6)uyISEDK@~Gu4ndNT;7BP_*5T~QIK{Cq@0%k&jBH!fX90YW@_Z- zhyWxs=x4U9Ik_+aYY~|k&77} ze(*|cfqgMY=LE}~u-XG}ePrym+HM^5IwUczxad&uF>DO-YR0HBjc+ebqamF%9o*oj zTwn|UR*YaURHGMpo1*{h|JQc%0W&o8CX57~;pOYrUkoJ61+QL({fdAIAzzH`rpIi0 zHM>(TH@a~W^1gpgYep-)HYzlfL_J~0Z$TWn}dRbS0cDt+d)T(kp8S_5VdtxVwMhns3s>D<5QJhf!Pm?`X?C*a)3%18V>g^AW4I8Xx!NFf)|3n49`!U^KCBi)bFta zI@T9pMg$~oRCFvI2|xq@{0|R;$PtA?p$h4QL>vCZpx#rmW^$58^I-jg20f2TAk?_I zmUGXG&OSt`zA-3|EPFOd!uPLz^O1%@qGr`7O(Amt|5sxAa{`SUMRtB9;)A&2Sto^X za~LtrhV?FWpMsMD512UcvN%n)0zrQT%$*V7Cc^s+Olt$b%PIYf&*R2mo1|xm4}CLg z%y`Bzt6~3?Zz=2tT!zwkhx;-5chci7JcFT^Aqt3)>3z!9*$v@F`P~?!}LPas>D~-HivvJ}*nU-20eT*9t1V`*}zCbKhU#XbNlWDDv zY?B1SG8ufM!ZF_Z2#$G~q3MVD7V{i-?!YEwpVmp28-fAl81;xDMof9C9gA(2z!q6B z4}v)nc1vJKo3LRk+I<8ZD2nE3bD=8cas7-r()p71Xm$2{@KdS5S~JA=-oAt{Z{|u_ zNyN~Ot}o9)!wFG@}6hKd{R{(-Qw`Zq-ZjBK790E{sC_^n)3HDLK*` zJ%V}tA^Ar8Eg=2N%LkSfxxdp&jlZ_9;su!nGBU)Wepd$2pxF4-XvW$iIV={%gM1#S zSrz6`L@*Xy4!j}qvtZoE$i(!Nuf}RLjT1Y8hV@rabN81C;+5jOc#36|6{R*LU(luT zRYHeSBE+F@&K6*p>OH5e)SO-ZI}=2Twh73r!Gu`Z%e)6_)SJ>hOpjPIw{&GZzow6a431HW#b@_i1lq5>epZQp32g zl;4m-61H(YrT^xu%tgKO1w}>Xpj<`RNA&7#F+r|V&5gOa`TBePI#5rn^>$1dgoFF_R={rKOF0~7B_u4q=kW< zWrSF&DWqv(pf`h<^fPqgDM0H9nC-@^hoa%w5tVc9x zres&)0H!P~w1S$9%xV^;d%r*{D~Zz^i7{pXWmFkt*WUd=T^jPW_cq^XUM+YeDrjPi zc4U2GhW%@|U?CS|h@Io}Q+*rbk6VIK2~S}btM9tO=R#Blv5AN2(I5txC-O)W?6QL+ zaH%hw?q0_O%s^oe0f`E=Uch<|w#%v=vhT{`U_JB_zwq$(e(JLA5|R6F#;o_Y*W|w?S*PT&vNe z_*<1;c?v#4e2S;1^6NVkG!_(`^~3#qt43+uMpc)WEOEN%Pw>ZX9`P_iU@4!yNh2D>N+T7!V2sD+Ab&0gz!1xNj)MDR7S?_|p^#Z&5&E zBIeyM{?D??V-0pE|?WnO*TUh6IeaAkU6t8_)u7@W`a5gFIjSQV-k~nF63v01!5+G zFo_TWW1!!o0v1RR`UP-Z#PslRHG*sKPp^$ilV09*a`RKT15^cigRF}#97UEQNX88c z0T|s=-Kg{dj|C3FuZeN*8ZziOB5-RYBow9ke$SMV(5OajhpwhuEjwtYKylk@Tj^6kUh(SSN2K}sRYz6KKFT$Jvg5nT@ zCnyvFhZ&#ov4jBZ0LSbNjIe5+y|`mJrinA!xO>4u<{St4-v-da6RAJSQ%aNfwpzP< zjD;5z6o*i-n&PFpz*BP7(notIOsr#kEiOQUk@SVh2RhHx%p0{d64IPS@Ujz-q0$Z7c&yCRo9)5*rKm0H#uDGs$*)(e}^-a>4*|Jr73Kgm9be z-w-Q)OK|!zi$M{R5eofP6-5EGvl27m$00NWp5H z*DXs`C6z;C4ud=M)ZwM7Uj_%P3A3PVBSRv$I>XCT7MaiY4HQ|}g6-6DF*Vkju5#+_`$h~eoq|Vw~er#FwDgB6w_9=bfa~jRpR$w{XBb;)kF4819 zD}`b@%W$L&(`zM5VF_@x0Bzc@bE!OD;|9?pb-|_BK(H!cC|(0s3xZ%nLJEz2#RBYh8R-(0(W0 zXrred^^f(1qsz@GFPyhoex=CtVb{(U+-7}~9Z9l}tKPj9zby$pB>%2)N?v^{0d6$Pu*O2I+fy?V2V|9X@<99-N2t3 z!OZL_NU)Creb1Y%6JF4u=_aI(cIg5*OOm&sBwo>%K6cd!+J34Bmt*_kM=ubg5X^)u zM)Hkp`O<41T+j{`Mz1WL{Zae(^MEEzK2Yh4B{}MUzTDx^Ui?*tJ2zAkV#=W%Qr%D&S0s5iW=;^ ztW;nF0y#A@WywW(I#GyA+3C!?2uY^TbW4lYVKqX&v!m<^cB7B|QeNb+jVe5$6HKrd zM4NIjvN2=$mh1dIQ`&G_pzndQbSv&M#EVCi*t|*iKGuOcG$tFnw?DdbvXjq2FH-jm zA{6h;KdQ+{e=a{DE}{4=i~5)s9m=mp0e`N{#VxF3o5vDsCcsfQS}Ql$YDR>3rF)N* zrh!JtUgs`XS~;^g z){22$swa->>V3;$xjijLsT)12`H#8QG3ST-++^e?b|+fFqcKu`yg6I#g%OuF1fBzc)sO=Y1(0!|%SM=i0Hfr`69W7? zZ|K$`s)o*;NsYDWHWI%LS|@g&%#(&U6MVb!TTUO0_SW$r@&i0(eXR3vyrV6T<{vL6 z%^F*XuA53o-0s4qBG&mw6V1VHop*Kng8XpHn?KtZ156v0mip0wYN?!#okNYxn&_P| zG5oBB_?5y(ENM(Y35KbB1lM$58Hb`$8OMihOBKaG#F9iK#Ay&LB~SL?gQYZ5 zd0=Tu%9z{a{8Q)vg(0j<|LYvU?|t$ApThJtk`zb+S2zLi0iabXv)lz2dt+5dNim`- z^q!(ZWEp>*V2^SUQ-cck1dJXIE%ZOZiLW!Sp`9nID93# z_ZR)XaSK_SnQHjZSY{6uGgVG9tnFeJfyM?xT&}ps_ZT*)i~MJF{8;#n<2l-~&&n3M6WP zq`Zo?8Dm(F;IMAz>2pxOp7!Ti$Bm?rp(7zE?Y;jXoA6xlu?zc2P?H@SZiVwjm*Lh?w09S*u4*2Lun+oB!toPcT) z#xdwM@Ez5on3>*YR4e|GC2f(PNI8^l(ULuKGlXZf`M6LS~9l?}&6FK7Kj+Fx-Ds81~WP~VR~G(EpKq4>d#Otn|P);NI%9{H;1qKiDigZXKI z=6ge7*SQtnV+VLfG}X(o;XFFUZYp2|(RvFK@eJLTNwg1obwJ_{lc#1_4^?LI3g}>s5RK^p z`)Z8l5z*E^Ud>%TlI@btu=CEC;c`CNf?tyN;!k#B9!Y9{>=c+0E*I1stkx03!dB0B zb>i|8X~iwPIKzO5_4DT~J**yGa1I$^JbeWIR@Ib}i4LYK8n5@(!2ML-A?qH5g?bFe z&$$9#_g;Z8dMI4{?x-haPTgK6W?^SOp{x)#s6s+$-59%;gB5OnWv+wH#{Q0g+OKHN z-P~5kSZrwkW2*JZx$#gRQTBpdy6gUG4;PI*+;e!bIS~D!HJCy_!C~EhWZT>kg8u#d zRSXl&x6O*$hBmgCDvPUBKk0@?%eFSO>P{%E$;`KctYRNyVvxTSpCkRZ1%fO4|2ls3 z%19B{7w9v)i=#js!RRH|F4MC-yGP^?f)M9kU}ZySbq1|o6bZ~~p9=?)NP**M3Mdh` zZwqBTLMIrKjv$dd){%+dI0{B1(hYP$=@s01qA9rH%lvmC6zyB($z;>v=dZ3D2{Ij~ zN8=rC2LpN#zf&=qK#3nQ!@ln(Q#v-HK>%9U-(ufB^nKL=fpBr;3mQ_R-!3PpI-Mqa z)yu-FqUA&JbKi3-g%rzBjsERM{NsLX#G0||{!r(2Yd>iA+t?Kr@SP*fh!dTz zt@8T)ofOF;Elp6tD5}DI#Lbc*oaX!|PS<_o3aL5A&=`aM@5r~Z){!Hm>5wmqIVS>k zG^{Hxa)Faa>bDpa5*7EWo0a^Xt)@E6$!CL=5ChercKzxL>3#S$!|oDQ2lC-^#1OEh>cm= zWjo>caJYU@V>>Z-HXHcEb+>2|V(b*eCW-X|x#*(`Id&o5@h?iAjcT<%!WcnH*d zGD^JJLRbEip~>Hiiq%};f$iPABol^mP?5jnuehZC4~d+QC{r}L{x*|wI9;$lBBnM5 zqK12dfo67oPQ0KXu$-_oe&pf1xd~@e>6i>0``AWs&{ea;0^{_hpNNT zoQmQ3B}wom#eK`mO9PK@A{_3bk4%HJ2=mO=W(P2= zK~kRMz5bXrD?7g(IZEUK6UJMLH-3>IL)?42%S#KgOHkqm2N2w1R&bbfY_{=lEwrP0 zH)98cxQ{rgrAiDWqoSde@9nL)(}2(nlSt{@%7^VDYg}K|*KEh|ZF;2Gc!tEoGxJmy zbcJ{MxRD&l+(T8=-dd-f$Ot0$&bD}@}t{UN`^($CJ z(kY=Amb~hnLjA@uL_y{5o(=BEeZ6Jz8dn>&%#h#b&mVn{>ydio2#+!dg-8eEmF?<(6*k3Ee zibK*R9*^95H6pqBR-&J{elWF=*}A?!x6b)&?`xAK=k!4R?M(3*qt+id(dCU$!MwF7 zqcHO*rknNz!&m1E^afrSrZ!^;buNZ#Ut`i`OJ}5P8@mWRyq|ro@oUYdAIx!Zq`vKI zHMsPb*I1I(;OZV89mUzDFX}7+T8I0?6vx&9D9wUGLm7mHXEp>K!7ZTf1E);Ptl+%~ zP(&fGJ%qBaP(k@xcf&qG9~OERWfMD5`eSVGngiFt!28xk9HUOtg9T+Lsmc2;02y*IOvW1$N~40m+I|rS z-juhwk7H3F?$(cRaUez2k|aA5e~=BlSx{BET(PD0)Q4z_9-@Bs<%|->R$xeocbeYm zUbX3`{`Tu`(11WjNyS&VT8-|EW$T}#%u4z|U`v5!Cl(WFH`v6K?oc~+y{zayIgC{Z zB)3(u`N~4v?L&s7d&W8Hp(Znm9n%g0Md|0_qDGFkjwA)rY-X?L#EL=u@3BS(Hsg(d zNOZ$mrSQ-u1pY3p`adVb@eB2}|GoKps>zPVq{+*Th2nPb@>=sNI3jZ4&)z{tS*^b& z;Gn?CiqRmRhK@ryC1MT0OX_eNL>wWl)cs}k$AzfK{3%8M5cTp;jyj8pe3aZqA>41K zqQlL?=ZsP;?`D8EM%9f|sD=kWPp3__gSMs%|DWaCx)P?w6FMS4GGtv!4=Oy<{B1@9 z;1)Pr#X zQ)Hhj-GP+<mjzZ>3=NeJ8@5#cVw|7D$a!Tllhpi;*Agh;XHDVeqzPefN{S zNtg3nK7R4of~%7bcSEo&%~CYw#ZKRa|LTA#?`S?B_*dl| zHeup?+biqOJHziLUp>jvPw+$b9v)F>)1f<^39UkPwqk}rJXE6++|Tyr?u$-j=+uXl z{ArDOSKC6aepMoqry{QjNpwFv8MIgUF}xA)n1kw}iWIZQ94uArn@F~OJufk9aI1;W zA3V8I_))dId8Cbj70nrA0EF3KGrx-VeNXs z#P2~kC=%By|8~M~ow}4_ngr+b3s%m@Te+yIH6xOGgXx?F zgmm6-zy7NjLBS2`zg{4s0M7KS>l{GnBSBPjF37Y%q@3#2zBjGW^w46$MJ@pNF9gW% zXz2GGG1LzsDrdw_9pk)u4GD<3Y1f^n!-hS@m{uj-^9Z`az=kCs($U&IC zU)%5lismsT{p3Con|STMw3hNvaTYRk+c({n5IrtxCsD=qM!P1EL5vTeeP{yzsg;Hau=bDLXk z%-N2yN?f12v*{KW%l)PIA^oD}xx!L)E28eic2U)wH|k;yRJ~Z}M>w!AM_}Q>$jz-D zlp{bey_*T}^Y#Dy`3k^X;KYJwQD2KyyA|Bww<1Vi6tht}gPNy&)Je?flCsq8_6SiZ zs+idxPA}w__#Z>s8&k7uZ*Veps!th%1G>n}Wqy}c9X8IoFkw6_rb3@5RA>vLGjz4! z(A#6ZJ34$Rs0R<`(FiU7DiMA}t8<(q@Tg*bdCF+aqWQ_J^)0uazn)R2TKD`?QQmBN zXAs;G?P;OT!jn`bo31yxGwM$y8MEGNc~oA?FrO^pRBfBTMUk=Mm%16v26np|JHhFi zxp^k1n2+uvu4)lnF_;w(?eDox?d*&#<;^5{VyizR4HImLLj@ROS5G3a;#haL;|N=+ zfJGKcnj~0P!2}Wn!T69Rd9LVrr6_Ch7Oa^>+H;Q^Ivoj+fstV%X(wNq!{!z#Q=_W= z5!SB~tv~eqKQT>8M~HT&d>OrXXYxA+E@0QWcte3sp8AUczc(m=AhTA6YXJTOt^YZT z$le>}ofCDZe}5LncipyU@&tX-NkZ^xSHzPB^Z87+H=f;|{A%94zH>&oZ~EAN=RCv~ zF_oe!X!FTtspm#7qk+LoE!yKJ8Jp!+3q2;qREfl*;`4cjyqFj4h6CUoGj$UGFID{p z?jjviR>ITEeK2pqjsA_e9YYOG%Q>O=N$<&WO>q42Ag}i9nwS@otZCyVW&B(p88L9b zC^MDE0`ssOfS8ftic)~HlKKLX=Tq3jJ_WJh#9A4NUq=;@`4T@Z=R~{+OoHZ6|O{L;YS{<2*c^48H zL;*P?w+;}Q%EL{S`4;HwEYnfhTuSpL(yT*g4L>d%;Gw2v5%c<$7mfblJy&fDEa>$I zi?ZJt9a2CXTg6BBFw##;JrTwm`91TldmeWrlxkZReTy{knS5DhMf0W^AyRJMS5d38 z?-bGS2@L8n^;KdH0EMLdKmI(EW07;8+d4KK`_-YgYWw9hdoh*Wm6*cZW0G2WWC&E-tVe94$$V2tiS@hevm<3J{ma?4cf zvp%}I$0-%hVAOK?o2ow3ku`0stY7yoQqw=HFL|xgVY;XhgA#3)>`{0R^cD5bhyr0H z&pmvI)Y}pXV7*o}m@c`|4dM`HI-99x`)u|Dc!Qa|wfIL>^K%sS55Ft&wdANzA=X$A zV--x;PvCT-MehXTz&Io{Xc7d-e;Pn4m$V>B3VYsr`s#agufo=Tmh@OWIb5t_>11g6 z>Yo$o-tjiyTiu6VW0r=)lA-y6`uIggxovGN`yEwNJi;}C*+Pa1(>I5&pXXuqDLiS; zdgn9`%pq9wjxuL2+CAtj-_*sSFr6QEMUwkE+x^8&N-`#3Kw+{~?rtq{htlJJ=wa)2 zpgTu}E@cl(dt~9$weFf&4ptf7{i=cgI-#Zc#^z+H)Jvt(D>tjU!?#XK^Z4T5J;bj$ z;Yab$i@PKYz2S=(9PRuwn_8QWhA`3O1C$uN>q@Z3=+*3F=*pCGeVor;(kPfDNx zd5hwgWr3m}Ao~&D+&9_Osl6&?4&zbM&EV&cEzYx^I`mE_wp*Wo;rB4yeR8`=0>$d- z#j;-IEr*5o5B&W%F)zg7FmV7DESnhl8Ac;+{rMd~B==iX=%L*hQsc!BY2O26Q4;D- z5m|TZZU)XXQI?P_8 zc%XOaZYBzm@;v})w0n-$H$YH#E30Tb{T`6O>{7zkkj_TUZo2UDn~hMQenL6KZzRMV z62$+5;X$|X!qE3y*IB4cOhC$1A9=#qD{x#dYkRd#J`3;6aA}`n2htNN8`xF)ej(-I z_?5xQL|f;ClBdQE>3wW~`S;+1zMozpoPAH!dbTFMy|;;4`#yAT_(4KquoakO58v~t zQ<5cLOmFbkbxb~Ie*fEZOMF4Kieu`H#&ga7j{if5F($VO{qMVkd73uH&;vYR)XY)7 zY~h_2PYQ=!v})u937J zj3mdra2B1)R!bl?Rqommd9LN?G*!}aS1^wqf;uR{>R?IomE9o7dO07ZNL^~Y78{Zy z{sJXQSand5!kMQG4}vaB7ERQzf`ZdorPC}|%wL!wW9XZ2>(%Zc~e`oi>r;?dNPVUQt(TokdSl!YN$ zPphdKz0%dBsBqncU+7Jp-I@-&Paw~%D7iiL3H7*|0}bDCpm4nFZNKth`CVOU;PV!l zwU;Wl#fRAM7nNmMv6VtIji+>jDL>sqw`$S+3X3PR=Ar{H)l2PXbbQ8_)GaScllci; zk1!h*wlwME@D(#sDv$11!65!l=7~gkDFF(rcC6w4q#=6%uy$qDO0+l24bBBq%j|-n zV@4^ho3JpT0e7AlNm{)CJ_XTP7k3-uRhJVqEJkn$2xwGlTn{kS(I-$K7I%lON->{; z0}_fG{*)rgGJ)vT;FGbv*N?x9`SGh@kKfeFR15^)=+!h1#Me9){;cH2g)ZxCuR7P}JJfvHQ+=xsWll#0e;w*0=9$W-AZ8oyCp z%cG;z3v6CgyP>R7<;1s3Rr#H>V+rK<$7p_6PYya1Rro0?k^B{s_S5ootb_7LlcV5va- zI$x+a4R2~aoJu%ECW;3${YMxB%reQ(dQze{g+nJ#eloGh{1;^~pQ>qO;$Gvc?f{~4 z)xB{;E^55h{m>k^eh$`0+i7ZH@btF6T#~x(W#umHVc6M>$+4b#TpTO^^=Ixli+of` zxafi^oZf$;E*+eC$WZ;(+`3VJ+^!#JRH{w+y6b}Ikw~Er1AUc(oeEu95$^b-*%*>g z=clHm@<|T3>8n4#XraO#o*P97xfU#6zZ73>D7g*L?t*=Q;8PWLqH_@&q2W*h+^ zJCnad$$C@pxEF%d!!qT`E6@PkvMzqygO{FrmCo~VM{l00=K5`#GsNAEL;u-53B6x= z{Ou*x*Vfzw;IT=m7Ew?fvBc3m{~(%tmxik*g!}jd=83JLGE17t+x%6;r2F6a!noyL zF>7RM_%h#c%!j*74ebc3F#klEkfS$JV2`JZOo%sM(6VGIJfGFLruLf^X3GM(*vxNY zG(}ih%VdD)yoO{xER6YVD)pTDvId96%l6?g9E~}03-ft;MK}*rrf`$c0r~}L3pG}N z*pFsZtrZ2cbG(E9N6WMP^uZ#`-prf*EmzV)`H0;TJEbuZ0nIbeC6ah3%vCw5E3Lu4 z4CX{wd)GA+|Febqb@0cs?O5bY$%|9^+u+#jMG>7)E&IL{I5b~Pay-G7Pw$=Vjj5P? z85*(=hzzyliRzIkG1&Y_;YD~hhNYAtOCXg<9p0#1_E)>jC-d7A61y&Z3V8a)cv{V%GJy|09Hxj5ZE0GFbhy@@Zl z3zsk;@6$fJx_PP=gt#uTZeS~Y)6*g~E7obvT9HnU%BZvn;0c*$?Hs6}dM9PrmiHG~ zOWm6VrEA3>xz>fOR{>njvZex;*Cx%Hoi=(8C(n+xoqMFCR_Cj0?eY7HhvRW5(%C~G0@}hJPgt>U z2j+&oO}4@$g0oiz(0dQ!Nvsir0Od)5l)p}1E9!3jXC34u)9qzi}tYeZql}_s3Q|tY&D&DxJIOZ^7OCeY>F^T&6gza$a{m|{@G`Wsy(5)|_3kdhB-mVK zAx-qc9p9alG!wA70s9+EmUB7^ppJNS%61LcVGD9^UZkc*h$?GCIyU;mD7nWQ$E*!( zUg&?)oP)=HkTy59;YfYE{r1CCTQ6qxIa~cxotVV267ApjA6~^I2KF%V%P#qgfhv(1 zI2_CX^g>FcMOq=uSa9yob}{J;ygzCKYeLGK*A9ksjFB;`VqUrB4`CWN>XUZJ#BGko zij$J>QqZB3Ez3dsQ<&XOGW0Pa(sra5KNQfIzaX!4J^WR&_6DLe59d$MOtz=7$bQ_u zpi(83#>^F(nq04YAwb;vsX}rh-FtD@G_A0}dLQ^*HL@~aL$GqEhm)KXEjOqNvOp1f ztqXfi%7reoKD=%&--2Pjg2te7k_CYP?ifkct;)&An7-_T=+`vc9wwXjHh@!(JAUP# z7$F<-jkElmRrsXDdo`#TScLV*3(_)XCXcy9S_Yk=MvJmZH?`BjL~<%?eeJA?1*IP| z*|>4;^CgA!H(uDb?xHb+re|zmnUjn-gB#Y9zq~Cyt3jBV`@@h}UZ9xF#G!4*b0#D+ z^ff)T21fb%+9tM4_>$4E*z%s;{n@CFIs!H8;k$iKjhm_e9DH3lV#5YehrX%ZuWp>)_57En|q(;!&mv`t)g@ks!qb|0Thv5~O$o^Iu^iA5A|ILmJ4?7mIVQ z#|+He`@SoYaVw*+1}pm$Z*DOgb{Fr%y_GAHHaaxi=B62Xteau;;3dzH1s6a;=SDdI zJ`%Ms?DG>ee_>;e${sLerF$N3wY>z%SYFs-Adf7$dA{`q;=(VA_vTIgJSb>$aUD&x z_MHMRCFKMgn=pM5qFDB7X;g}^6n=V@jorT*w3crgW;t5GR*Om8O8-kL^qj^4;>X!n zb69D=JF{$N?H##kO1xRgx+$f&#ZFxRXt+@iaRvA}uwc{*2j*gUnZgpqjVGY74d3$S z;Ch0#d6DSuJ7v&Nkz^+~UGry11>9?9ut0=X@H62;nr~Ey?8wjhXTPHZu-~SdBwd}Q z!~GKbrUJ}Fng@hULG>#fjBH@)fq>NCf&4OD5DVOubV0fLyOTZlE1@&XuXJTf8#x=} zw-q5eg(X|v^|i{m_nS<7N~NDWM}yiaJ?H~xalLG$Krd3K!YLro_c!qk;(!|T~S0{79L??^YlCfh;ZO;rRxpsol;ylwk z#=uJe<@|U06*i7-xgR;YAuXpDBWpCq&UuXIog~bK7cPVIpX$Y$vZ%JN z)3epnze;dDOi1JscvL!y&hF^yf^g$Z2YqeMls&FqIBM&=)6*q$)G zDTf*sMHz#>6z7s}tJW*vW|&nFXM?DmHo-tH&hkF+SFMi=U60r7I zgYXRSd=38gW8u0aj#irSX#TeyYF&j%46G0(D;k-tlt#7U>~lVA#N^O@k%w1dLoh2$ zW^pi1EhlU+JMjOF1{_hr!AuL3lG0IL zOd_?rV$O$8Pre)Uvs!Q^n{tf<1mV}O*K~}e8E!#_i5m&XK{Q`hHMmnq*c}`>GHR4D zrqYd)Tp6qWx@sfEl!Tv0sf-zs^vMOwkj%ZRuXmHs7A{JZ4*mH@$zpHILOI2a9Fn*F zmXwjcTti*=oEpN<$$`Qe^Z8|NsTQrDy0Z-1`O_0vqpZKCz(L#~`*s|3=B+LAe^zWn zd@Y_3T1XuM)aKk^@TEtVecn5X*MnYmMBcXrz;N=nebWBrs$}v!Ur6G*T34)BUlp^I zfokbfrc==JvKI5^aL2}r)QzV2(1N}PlELL^7cuHLtA6>&IwJMO5-B?7PyKWl8Be%Q!ezG?S10<3~lq3KC&FNfyt9iR{}q1*Ny&#jl~j%{``1Sx*J?`W79D8>`6}|{ryv# z=l_B-0(XWi4FXEEzVeuC1v}JA4moqwaj|2T+DArEcUN!vIT0Ntzgk(rUdrus_^8~- z@VGwtrG@=(Emx6Vh4o!S*2l;fzAL`=pM-Gye(jgiF0!B8gFV9q`3QgUBBX9YP;FZz zm@thgW(9W|;&&xlsKHGRU5mTeoR)P>J|X8GU&|Oxl$4!JBXwDT?fSLtJ%UyH8ay4Q z)w16wdyArC-iyMWP_pINdVfxuMt5u$sX#&_Fx(WVp({Cgye(1qZN^_!ekZvEn54Gj z2H}hTiCg5XavuM&se@sl^qZ=IW7yna!uggm@Ff~D24OR=Ol`73?7=h6ZS=0d67BO& z6F2*^i9TMj=I{(-Ngm^G>YZh&>LE$iPO(+iY*5K3xPSIEv;^_twj`5-PgU3>4UaY* zchj(6viXm=(J{ZR|17_jzaX|e^{vJBhQ$4hoU1_2a__#vR!4~n9S&nH8KjgyYTW9- zUpI&NiE>~WwJ_&Rbw5@Q1r&3&uTe-qXUE_5o|H`Fmt!h$xxpzw!4z5x@mshmWgH4R zZR#geC9jclTcVxHvyxDY^5Y{%5EY<8Lu%a+E#m;;hGq^T1f=7GyOp><1k2OMkPlsr zgC7aPxibZvpU2HJR`Lx`4gIPF!6SEg8mJRfWi_O%9d-Cm;M_w?K7R>E`Omwbew+x`%tl9e706@_Ho4>4iobBhpCUEI*M_bvB*7==N}`U?Yp(*z+D?@*0i71v}3z zyKGbx`)u~zxN2zCXd-9_zePPW?bKHlek+t|FR;_chzR8vdThvTTFnV!2yzHpQHIz- z>845`!_=Pi>Iv`3$83{9f`6cGcy)5+%Dp^(189xtS#kj_c^JR+2ZEa#Tt!ECr5u}> zI-2k3@sk_v)WnM(!SSnneol|1hVs}&om0bX*HiYl^UQ_k{yde%RRn{8}usrJ4*TyKI*+xA?_5=!YJ=?x@X&1Q2>O zChs-kbZhrUj8?z$O0PPdY~PNWE)Dxni!akqC&k&&a}TnDfK_9IiWd=x*BPrPfJ^|~ zcf3*bCCa?V?uWs&15zGlGiqka9?`-YIwMuFl`6KE zVD0rwU{wA&1TXa>fAWYQF=+r!W-|w=EF=vf=9QQsKzzD`Jqp3?6Oy>tNA85Z3W89# zzx&z}`nGh2OzZnb;TZ*3T4?lJ$H~IPC$ZY*KrH=gOvwAoCW;~JMCbZ+}JAk@XY zzU~1fc=`pKEo_FsHtbP=A2B>m9m<>_dUt!DVi7;^^BEs2?6BFG%Q)V1KeLg~ z$Ea|iX|V?ir15#_^bE zc9l$Czb_uqv>KiFoEYI5;PLP%YHVw}=DpA=)kl^x8ndrgGYmgz{Ya0Nb`~}1)MWCm zJTuByPyGA!bCpq)5Da1a=lBb%f+MUC<{U>JIjxnpz zoL`)vYvcr0^!e|8FaHBm^a9^EmFQfyxAzSIw4TbWcmoa*G=d*w3!iub+64o~GC_mn33v3OdX)L6mu=SETD-gnvg z6PfStmq^p&vW&6LcU~7&Yn5*<7u>;>7e4-+&-SH79~MuE3+ud@avtUIa;NOUwnEC0 zWsG;+WDc^#u{rmCoK%uGbN3K%CwE4zA)YAREE==8or`@TTL>vuq{Cm_B+kD2Z6J%$@9Txk$ev|0l^nKRy9w#N zFA{2K9TjLu{V_FV7bmI*4x9?vF89uTmgbk{4ucr%RWe7s_wI0gMlxLdPLVv{L$>mB zMX=gwLc_QShT$3C0A1E=Ssc;8lp4GI-64^gi;p>f*B<14HtewYJkYdf2GOmI^##s- z{ipHHgqj~oZqW$Gx`q3h<#M&Q`u&}kqNMlS3&tu(Q;!|($Uvsr!GxW;QqL*VySopN zyuSOWZvLL?uSy6R6ne=6-WuT(F$hE8d#>+W8;IyxSbp?I5#s*X#ueDt7O z=iqF2cu%!Fa`~B`B{bU|R@_w2Ul=`^sbu4Ju^cFUllw%o)Sb*P%yg4rvF4ss#Qfk* zcZNH*rE#-*O{LA)2eenG;w0WSc>I|(a&K^>o5jh;swvMJZ9Ny%M)5S!pl}`!v!|3k zq*l8E$o%3g%=N{#kwLPt*B6#Su2h7_KKw%9$LjU|D3qXV>K! zx@5?kiYP*Nxm6ary!_nHlyobWbiGDDtXJt4c@A;C9kUABBS-Tm9J@};LBX}|saayG zii$D)%NSuOHNjL}gAbj*q9~%M-6m*?5?G<5!ASq;dcE5fF&f>6OVv=NA+#Igb-`XT z*GlcvXR9}PBu$k*QKCU`xEHY50aXwU8`{3I23tEl&8un@;#?4ppRpbkHs6_YTFJIPo~w}><88E&FOTU~?$fPwsfB!K%zFxL_kC+e zz%Fh>qQh zz)u4$BDngcbpoaK&s!Mu?tisASB@z^bEf?K#`Ps{hP(iV_p##3f0GxcpG%)Pjy`vP zm)mY}zoyKQF|kvrIkXyDcBas7G=IqYB|L3%mB?)Em&uI@?g!|sGgPO@rj)Hn6YQ^k zYdX`Yq36w&DiRGVB`Q_EU#RJFc%Y4=0c)GeMv)!8v1c84KYDt5YI@_i-iTT1cPSYG~(nj zw4SG_g6JjZ>AlL$!)QrmSd!#^sG1h&ar)`UxlmzmIWdm%RNU;Tg2kJOBqPq}oKJ;> zx*D2#cd)3Je%RfXtAD_qt%c$jc*;_d$Da^Bt6$rKtT`RV(sHB)CKWV); zeb(@xkX%AJV=2UY;u_CftBfytrn2F|)(0Fpe1_RgJT9LF-7O7s;d)<8ry;XapBT6u zLVjHPv#aCryYoX5yK#@hqiWA+ya55aG>Y&uBkep0%h+rS5mPs)Ae}3WwoZq?{Q5wg zk|$zYFbBi^;4&M`yf=5W=IBw>OKn^J&2}q^d{ac2+qs#@!SVQO?%Z>o$%Ke> zX{$)b#S@=d+qt^T89X8|;G`Mfe#?#6aC5MK$ zR-CigC13FGLbfVhGRc{;_+1qe%ZWVq2OS1nXE9N}Pe;?9YTrmHXM*6+?_A_*DksyX zw`CE)%HhjiicZDCq>FK@?1S9?7W$7kQkvGAjKjgV?&U}-AyLPTTkg%>>rlr4*tM1F z{zusk4fbc8CnE6B7@GY^HAc^4Pg^7uef!p1Z}RaD^mF&OGzq2i#Y3Y9g>jSv3+kx* zR^HaiCxQBinWDr6-Ez+63FjJGMPftr=((^imEIuUocCky8om8i*a8k&IoSG*Ahzs=MiOk-f)_&l)#^;W-DniSd%k2A{ugsqC zZ{aQfb;jfjqlFqJLDhzxc_#2Ai+W@y;yFu`ivLM2F^`EJmn}JcQ7m|YQ*N>vxZ9b= zNnnyb(izg?_oF}k3~szusaCO0abzC8bRg<0a1@>d&tDz0aClDHW+n7vEG78zH}sp6 zW5Qp7Vx_rx-#^ostl|kRg6f!~Pc>eUr+$Hm^mxb@EBT_3Z^?3r&MU4vrldq*Q>i~_ zCE|z9&reN}*`gQMXJrFIwD-P>2b1faj8gZpEcDzqddCjvYT&eOmOT1QLgqz`h=#~G z=}@`5|DJ+zt3`bwRWT{M-v-?5k8&`;0fzheU5j*arRX4k#v z-S};=l>RE&(rg1SkiNS>#av_Hrn~dko;2)1g0q6#+3HNM+%aXrPQ#<|)BVpM_d$|I5c0HAA^2+?eJQRSA*KUgN@Rq7z7!O^_v>>EIubJtn#MG-Y zUSL)>F$LM2bp=lr?86F*EL6U?k`F$)9t_Wydh9?*Bc|CTQq5!+rG>_dDeGu#KjcH@UaU+U8iir zgjBW4M(oa8;ukV&d4%ey%7^`Nc01535jxA%=>Xe z#be3DG2#g7Zr|2n>ViBy2~Q&42h5U;Mx2qswX&QTgh<0+RZJYCzMHWL9kG{=#(@zD zu(wF*+rm@b9C9ha=P+m8Qo77kn6PJXj4RR_h7T!)mb7zhk7~~3he-n$NJLm>^1)L; ze81}G_h;;zefZEk0fC^&8=f^j;#fX+`H49jxes5Jd>i8PIai*a}d7#QG_tA|* z%{F7CX1$9Uy|$c`8(rJRV9mK)pJ(uFyDJ?9(Nwb8dp_{`y?i3`Ej^Apo}ed_LUC4H z1|{~`pq`{>iNZ&Y5E%H6>nr+Tq=N#_MX#|IzPCmt7MjK3r?z>*lAV6y*Ec6GZVONJ zm28oG9w2gE$#(JVfFP-eop>yTC}gKl{>?H<-5+CozZ|ywI42Jq84_dk4?Wt4XI-Rz ztRZTAk36M&1iAl}LcBavc}7T1%^F5kX3PQ@YKrLfu#LP6r);NUEB0{|(vf^tsVnKHq*l$J>X=!ub#nYkEE&JPhp4OY z8Oj6}`8J>ZSjTXTXZ@Nt)VB+-bwRUzLznpid3*HblQomF&nIK#r}MLOMWrmmjbUA1 zB?MV-@5iSlGbT&1_Q4GmO)$-eQil+C)$4`SJoP_-%&bV_iN=pMi=4=|*R5+6L%aYlHJ-VmTR-3La`eC~D$!(XdxM{K zR&rb&^e`H%i0wc6$Q?l#5>Fdv>AKW)k*F{z{no$mtfqo!?3&Dxkcwt~KzH~AH=Q;q z$+eai;bAfDM&Dye&9Ak3VwQTsv z>F__Xd8}{BQW1^Xqj-PsHSwjg(ksB>CBvGZOQ=d$QT)iAV0{C;DsHv(+pk2zZPz2T zV9NGlN%^!V^=)3K2oq|3yodHw;`ZHjj7-JNC0X9>0$Qz{f<0U=-ye!)l@i&z)Vd|d zBpe|iuvwkS-(ZeC-s?OG8#EhD;EiK6szmdl7N z38r#})4cJ1up@Te_M`!kxS87oa;hjhDeQu=YJ_IRA(rUF(FpBV8t}Q#q|jQ9jAotr zJgk;xJtX1NEzx3NV^1~gz$y%kHqsF*7otuoWosV=io|v`$Ek4r~ER$)5 zKcD>M8Txa|ZE3*5c{;^pEp&e{=3~mWeG5{0r#{4CPk{Tx8N2>L4TOQ=FrFA+<+Dcf zl?=udQqW(+d3E>OES6MEa4GWfTY1E<`71dwPZGqL1{xUkkj+DWjCpi{h{hg^RY!tA z5)X{7eYnJBUwpHcn;>V4E4SkYifXAvCfkZMRs;V6E#yYGTne5ZM*dy{RomhI>CuKg ztH`Kx_^^sjPRzXAVHWg5>(yz}&m>LP&@nW0H|+JUbCrrcC?F>OY<#?nqB-M2P(vNw42Sxxrqy>2KDHXZM zwP9@F!vFW$JBIC+|4_{w^X4)rbuWXNu@lnWcS_}f(nno7$4oI{_ltyG6e=$bz;zMA z4(abTK2oC(8NL}vta~PWskAebjK<+2OkGW3Nxjq}9l>!b8$`VDZOkR0*2zO zYyn$B1ZtDyNAxtfh^Gz#as@E)L8F|DdEp8R`-lJVu?fl{(Z9-kMeye+zCaj#keVRA z_+?>FV}F)VlzFxXsZI#28fI{5M{StfLZ(&(REswTRZ=#}TO!^R#hT^*%p5D=enC3` zYvi7f!i`aHtT$SqE_eD$9vxj+PGPQI{)n=P<7db#-+Y8Yg9fL)1}FAZ+y|?3sN)wD z!q`Cj zT2bgIaA6UPzZ*{|{%4|am9BYAk#pA3c&r-)Z+KwFn!Qs@%y_%RS`;0p?uEqYrj(fL z41IG_kEpmt;l?TU-^BT!3&$pmm33=;lsjyKp8vBvtKY_oirac!LcR}=TM zxTTRLQz_SBnSCO7J?-%Jt+vRfxzz71akXyQA4R2$8YiT%KZW&XQ%}QTd}}U7o(S`E z`z-zlE(GncKkxQ+X>dUMN2@Y^gq<3?bNJY_fw0fMii7gcXqT1y%J1099ry5W6?%_) z7;ko7l=~07CN@hK*SaNZOc8K8uTH$0pS=r=CZahPt_&!Zh&NpimJP=eoQxG=`sGfz zY$T+=`GuJ{H0rgRraCSxNaMYw7`0ofq9%lG)|E$iJkCbdKa(UsIYC@BVX4b~x^*U? z1F9otH!`m9#L>0eJeNz@{QisRA4dw$3!d_tlSVS6_!7McXXC=_-#2U`kXBE9)d9g{ zC~;1Y5VP4OYRVylcQR}b&c)!j(?)_>a#_yeBTdS`7;Mz%vK(S8_+uac~x#^ zfF+^Sdf#O7^<%^IZ{Bc>eZge)l_=Kghl*nXlt_NqH}z#+`zPKOTWVhtlkR zxz<(i)A&%CGcvS5a?nqOgv7VviWzq7`aH&~Fr+z4k|PaotGR%o)}@X3UZ8LpxMwm? zPh)Vm$GuqbSvjTO3GT#{fAL?gv!;tK^qG!>QLVHc{&1%XV&fu0M(RHoac|wwi#@5+ zl4!-_iG$gsRJ{rhs@Yd-JTlWmN4D@=u5-pIY9Oh96#rA-zt)k_%_wog)ra=A`t*)% zKZBf6Na6@(k03v!I%F{dGM?pIe^%CGCGb=~)s*2BJxGqRPqW4Ea$XpL`|5@Q+V>f3 z31}s(!xiQsF$HQC5gu#D_XxDCK0WO;Jb40g6bb7?Crx2&^e+HwpLV-hJ-&(?On*8O zBC9cbOi~i&&HI_bJmuNYk8dhFgLPxf6LnI53>ykNdbte~5|E)-x?inQ{!)^6v91tY zRt@hF6yWX>wRfb;YuDF4RFfJEtygvEL0)=Y*f0{~1)Gg7S~fEH^CpyJ?I~vDS%FcJ z>o8MxF#TQm^#=J(=k4>aSh>PC zi+{q6so`WfuJ^0k1p$F5Gvn3JsrqE9UIXs@Bao9nLcR!fUAHRK#jH~O{SgN5OY?#h z*9q|Y%OwZ@dv@NwK>KHWWA|ex*2c31$n)BH8^#+?%8?cOM;A@J>w#`HjrdhAeZ96l zww2PD?k4N(PsR`jIg9os+nR;)27)0Cqh0;Qga5c0d-ogDuDnf&Aw(b>cD}J$ePy6N zBzUTXK^_vd7Oqz5!Z)E0B^?uOItz`ej8jAXp4;OLqx<4BsTot|5j9Gv{5>x-7!lI% zHLYe%?nNK@`>*||#bb^6<6tqi(CMnrbWRsP$uD6jv5ov%E@d(hk(~V&z79hQ&&HHS z1sRHEfaJ&nCF*CJ9`39A`KDX6Xuws{@3EirSs4~RV2rPEWK}gQhPzncsR!x3EZKO3 z)0_A@pF-o~HZLpz`LT2& zH#f98<9oyUJIL9flL2^-ZiMdjb3qMdES%F!3qZoYPB73y36zWZj)fN2sMz-`Y? zMh2NeY#nZVVYzGFGe(T z_Wj%{9oA7VrqZ-CIH9pZYz%*7etFbs@qL!>L;3ghKVd?b^_F2n9F>ASeb|PCoUeZW zaeT3KDmE}X$BR9n=~-Yq9&WFil(bMlrf6YxFBUt!d$tn*Em4ZYjV)M#+$2H3$brzpy#N~O57kb&K*Ff-| zG%=y_38U5|lG61-ZbH?5Tg2=G5SC9@@_(8;H7OmMLUkL-zMS|Q`ZwqV$>U;cxF3T4 z?S2hPzr(wB>g%{R#h2W@;Dt}lgK7cWriA+?tV=%z9$mR(SwO#~Zov4Kied>Ju6iA( z$sZR>9LDW>?cs{&=INz(hje<-5HvlD`$A4=L*yix`^ylagdeMDQTlatE-vGz)Z!m^ z{2<%H=-OB-1gmOlyv%*R!|pH)lSlqF zs)hYBXBnz^^8OwPGDQJEj?ql@{r*Ne?Mc>qinJwjPske}_bp~rm4e`jQrEfRnDaNq zHqDgtU?1tv?`eCMtBb#H_bnt^9+MU~YHXL6Q*_&^=v^OWVsMv#7F`xIJjWM*TY-1ag#7_6V;Np67^N}i+Yx|1bEXJ;`(ko#umx{;jrJob?iZPCXut{ z4QJe`v=jF@w*yNfu?5@Am0vg z>(AU5ZVY-knV_h2i2h6K0S(CutlD=#Y*lQ{K{?N$a#=P7ea2^Mgw8B0JV=I-ILvo$ zyOlOFn@x&p3Tw?t6?G~gh-lB}!kL_%=E46V+%l9wUBR6FAVxA=E=A@XwpB7>_Q4heBv= zIWhXo;rF3M6F-TAP*R>v1z1Y@E7lgr+-JhE0V_jzmozVc)__XBo@`h6zR}DPI-R@) zf#{jNPaXT~vP1-9joRX8-*21Ijadl7$pS(33_JKEb!=B0wQ`+#_!-f(5UBloL$W!m z#kwAnsLeMa=U5WM@H9_adD^z5o z0AIREX{EVUY|9XW(s8`aFV3?}Ggk zG`oVXW*wXG3Y^Yc0UfEOW8x={*E5}I5d7|vXdT;-pqoEzFcXiCD(R=r>#U&3#Dh-^ zPEim?BC0%(C5nO|W4Uj?xjxax zI`Zi$i7I4Spn4wig-oOsF}D7o&~C1_!hDbvlnIKaSIhzetEDaSc1mjLei*YTfJl@A zT0M=j=)aAQ+`DKz7B-ort~D0v(B<42()1T`+E+C8+D3@R-*EUH6}<}t z+Y9kI!<1+aZ$hL+tQD%I>c>WZyXD;Wsq3U0UL?oU;fW{nQO=QkhVX!%VL6AfGtEa7 zfJfePPhHF3NUSk~&S{&6gb4AR>mGLpLW%<s0x;V258&bXGq@5Jm~hEFKnMxbNO`es@eNHKi~P7-6xX7c@|Xx_>Dbxag zQzc3PPqGf|B~I#%Ru;R{!Rd=(RC~?BvN~rETPrSaHSP>}`qs`+RrMXHH)&p#SYKc7 z$8UK*zF*3~fYQp!YIAmx99NO^R;Efhcp^k?*|G6C;_PF>fo<5-rPKrSF!`6fn&DmR z%Yj3R2Kg$uByjPlWjW#*bhpQSzWe;;RtM8R83&Svo7<(&S|6mnDTQ!jSOatqyUtGE z#}dfJ^!w}0k#o|fq4|fFtm&|E$+{yZ)fcKSGhoI2&NDqs@0L`aQEEf@T-Dmq@%OYx zT2qN*sq0{F<_}ifON5QTc5PvbnANB2vk&3x@OFCZLZMADKQGKz*S|S=a{-6>BrL-a`&#I=I5m zDN6P>0=+oyw?KIXF)=jHk>O1T)G0q_Vr~!IzLuGgs^ky0MT|bsSKvB(kj!X!BR$-U z7Sk^^Gj+@XJ@|S%b?$sUxyHn=a&Z)!78q~C?Lr}$??1&W7${e>+;y&Yc3EY_5p`H} zUSIlV>YzTD(rrkBWc@Q|Eit*J%lVZNkNT~y{7lglqmPAVo0G)U+aJ038BRAc8TYAf z?RJlUJ`=B=Au0rl>CSNP^9tjx$Mi}mkJ@f;;J1!riu@HmH&>bqO(yagM9eKpCN7$< zj;~#COXi_4T_^1}Cn!G_=`inGWu=P!F`V4R1$=A|kj4h2cl`tvnKH`|r5aqkkUW0% z*?8mDay`zSwO*Y|Xt3j7|KMAOG0T7^-Jknak1g7LQqYkVnm69V2LfmnF&~;2HEA&F z(!a&o#pYR^$lx8Bk@rn4ZZd=)eNNjDNqk;AS}*XZs`Uv+Q1f z6LlL&#jiAmXTYlgI|f;ymi98|d%7X_U+F&D=j*Iea(1EEvt(VDQOnpDKTCmrxL%mo zwyvALpW(&Oc>27710;YOFaAoB^N%njI3_vYg=}e#3Y#aAIEf)^b;=Ufj>A$6QBFwJ zpmn(9n)1G9tRUDOv=W-OJ$kkhp2G4}nvn=@xz{2|gUtgMW!c?8+0CIXfz zZwTwB4HHkEhW(T97Cv>2IzvOC^6)xY9344?o3zQRel7jVF_&d zjn&Hw6p(GiEj;05x5jV^PEz_JzcX5Q*$5SGPHAAEe$G0RYGL7Sz{y_9c1B8_c`rIF zc>RaY%40r?Uq>@>PU;$$vZA3m-Hi;wf0g9y}aiCa#%phFc zNNwFFJ|w%m&@1~^6!#cj`i|E4AW>jY=GVrad4&m0`u0DQj-dC(oEwg6!lafnai?;Q zn5v}{HRb=dm#Kn}FZYv@%97d8>zb2$e%w0uZ{6SM zhrH_Yup<|SbIN!#@id8L(qa2>usz#hn7;g{d#zRfwu7c^PZ*CBE%&w85)F8_AW&e5 zoJLuISR$W=$>%a+ls!!*mhgUt@|M!5>oMmU;)U)%!;QqCaHEYS)i6`}XY%!{=C^0(?&*rofk8=W5DEzJW znpSAoS_mp~r?)OTmQ;F5@&Q|X@Hh!YeEReL9p#{A-4Y&>h50}Bm?hJx3oYe@2U~GY zV)Lqd?&5?8$70Mzr@98LO@E-ZOz0wwSpi>)6+#eHT~^*SFXs58{&0kfqT_jL?Lv9= z-w7R#8JKPehkK5~&YO;Wvby!UCVlx9bi4z!e4HA^)hGNRZ3k$?eo`Z>KN?+1reJS8 zbL*kO;@G>|yGk7=gJXoRNQ&Jx9T1Sq9p-~lc{X(77MF5XbzBx^oW!`H=GyM=+ZOTi6}!KX&3vy#|5fdO6N9l)&sQm} zGHN{q!3_B&gd@j1Q+b>8$ijYSe@t(cd|Kd?f{M)s{md7pc5(5-r$<{*5tGsIlyt4w z;FvL3WJ`6q+yhTvU4$*Zc;r+feYAf+)YF+VJ*mQcPg=$d%dg;}9-;Z;b-h)r$0I!F zf*nJL4|+b|V6R_~QvR&mxAP1c$L0UmR%zB>zn*0Cnu%!{=ph*)CQD27o8o{X(^bjs zumt|$!Ebyd%k4_Ict9ww&*!vmhX%cbLG$I6m0YMBHw2m;iWIL{R?nDP9B~fS7-RyFuium^ zghq{dH22*O%7<9$WBmM#71$rl&=W(G?o)a08xZyuP_~P2L|kpakz{uNn*Mn^H*Cj& z1X#_ryPC1eHIkWm-yWxoaOe99&OhFTfV_6pJ7Uo&SC_!!g&{b;@MS+@Zy?M$Uy|dC z(VR`JHNy2nB7XMcs*$Ls>@J9SfO9F7YHAcY?|s25+K5dl<-TlITAhD7m${JIvwL>C0bCi2ZQ<4(CYiRbRj7zmrmuY1` zo%8azZdU8RmX}*`I2yfznd&mK*q|8LT+i7_RBx}5K3a6q1`h7?Izm<01Q^ma(-3e| zkByCI1~-#CiN35tdMla7QGt(q=iuxPQ=GVWTip0f%oSSmP?D3y5^MbltAoz2-hU1{ zB0{N6=yo%CTxA}=MGw!S$b_BG7A>Jo!$q?Y3Kt@Z6YEH;eiS^226dAN8JPFopg#<+ zBDgel7F077zTc+hp2ID~T-h4G=`ahqu|eFYpo)c+w((5y7s$3E)Ix5O#Cp^u_Tl6# zy0EOxpV;@_H(1btw1ev5JQOW}#0f8u8!k0AkdcXi{vgNcx~&dGUw8Ne?sJfal7zO-o4N?is}xQrJo2WaJW%z*CF(^^A`rMgsWE%3&TqqRM?Ow zj35BOdLJD~{A%eD9!AdGf{JhuQceB?qd0UE_#YM+8k zbk(Bt)om2Ga8s2`I@(^lkct%s#{U~fkN>{wZzob;J#0fBujHG{?|#~>q_%O7;9G7F zS-_*QU(*`FbScxZHpf_5E~L#~W=UFHfsL*#%&aNrWy%%{{PcSU%Ppeyy7>x(Yzl-6 zrD>;%f0)8>yE!P>4HLWT^PPVQKIbN0A7%XOEw~?N`cmwOB*{Xdx+sieNb3{`aWe#r zGPu#L)#6)oU12?alZ=;>=5-_+$Ew?Xb&6;JFjs#vDjygkQrpNJusBZq2wiPqdI~?r z^#O;RJYLp)U}TT6~)4sTB9RYavQAv77*SE013t|Jx5Ek{*`(_SPe$nxd zMpBY!TfA*5Z7)Y5-`d|FmEaFE@C}i$k5trby?MNNeLS=Qe(iVXpU`BiTRtUV&DXN- zf2hh9zub-9VEgB65Up^k0nSfys(23#wV{K(b}H3#S`K@4+`GdOX>L`RZzTY2m5t=Hrl8dfVqj=T^yrzAquk~c-sW*<@) znkne}pOnh$AYcF*;6++&i{Y2~&7q2RZn5 zC(RT2%&PT*KiCRtb7O|fw-5aMVk>C5HZG=ugD`)^-P;y4Xr_1B{WOIQn1QyJTBGTE zf>NdKVXP6CUEYs=mfp|aV^It7_!`i1$|)NZM$?h?X;)^l6zm!p*sZTkn4@L==IyiN z4ZqG$C+}fwF=N6Q;rj!+`ooffOZslu*AjMCV})+wUbt|)`u$$w%e6J83Yxu_{bvW3 z*e5d4A(;g40X3S0-<|7}4Lz%E|`A;``UKs;gswz=4ZUpmNs;-KlalU!iXn zuj~y$sqCGnpx{^Ud3w4v{7v1;iV+Qz1SsA7Ot22=gRsY+V z|BO;n;E3g_;q4W%baq9JyA-+a|Lb|eZmyfA1pjlv$g>=&|Bs;pcOP8rXNQ@BaLv@r z%yH=_aV3zLnp~h8c`HDUX8^r%1yUa}x2T(ji{;>%U!Q!Zgqqy<*Rz7zXqcIU$CQLa z*;_AYa-68YtLW(sL{Lizpe|Ng;^O1tocdYohRY4b{qQLIQUsmfF%f4`0||w-%6_(N zdw6RKWUs+?u6D5ax?v-2@zo_2dD<&Spoey>M z*w~u0vOv7Wt^9ON{soV0pd1`K+v?raxLZ4?ZfjdI3xmQ`g{8Yu`0Z{@Hk!c#V@+MlYQ%@v_25YN;|r1TW(HH0U@(yfh%INkU&{PMc=8H z?rl7_O8S%mM3L^*tAcQGl=kkw1~t*6KeS9VH1Brj>XaFC$7gG7Vn;01kJks)U^i#U zjoU@V>g5J4>SktUAJ(#NqTdt+mNl%C@>-3)8iMmBE7H-?6#~hrx@CjFu#PR4E0*B! za4mb-O)cmvIyO-OkW(|O(GgwSNQ0CmahgbfSbO)-WNS2cJ4Q`O#lGbDhBpwLhapy2elNUs_?T2mf4S#CJQ zn4hwIDa^tW60IZ@2=u%D)BJb&$6}J;Il(iwdCKWxUz<>=9?OBCLN;Xb1-E%fK8jbJ zO;Y0V3g_S;)Us1g`XR@=*|D8$ijSXy!sUA2?7HH4f=}^$-So5zw`pGo)Z#&s+;=) zL?#<9*ONasx-b?N7KV`Vhh=5aC{q+c!sHbcPR3zZeLpS#fPrpU{Eq$S&!3lqf{9>h zew7L)icCzz`opUY-qSM$G9>_jJXx6;8L5SY8t)ni@wLHuLsdnky)Two zbj=l^ZDm!MkDAZ|lR)Wxq9TU6FcCxUaQmPxEz-QtK9KZf9()!MQZo-Z3m6Vpo09t(J2aD~XrWQb3<)|Q%=H+Fn{9Hakd zfwG<+Il%SK&Q6cG7dRL+G&H}oS#t98qZ1M~U2zw{%j^Irz>+9vccLaen$)%DbY54W zS4eoXztGaK*N7nDvn2u5@iDyi@9DD3FZ~E4H;XqnEqUGd^ngPd2U5@K zE-uwNjV@Ne49D{e3f@x)IJ6zZ0RpNU8)peQEPk!jW|3peRT5Zw=(-lG#gsgl7e+3a zg@Hqo0h-4)WWxh&z?;(R;Ozuh(XJN_!B`*4J882*iHY>5a^^%SepS^Nr2wTKsuy(I?NF{N+&n~xt~&_^?F4$<84*oZqmDp`!YLX^)`TM^u`K_Ma1 zuV(#m;Pcxh2M55ZoRGV9&fxb!M0)zr9Z9-58A}v$WzsB;JsuKI=l(0;3vBH{i$Qpo z@e*JV=xxRo4uFd|@77Em0m5_d0Cd9fH!eSiM@O|QO|byrnNJjH09IH5L@d!-FW1TN$OYQK2<%x21_cBd zEq4Z@U0Lqn_-}5q}lGy^gN07f9z+o&d%!`~Lk);DD~ZtZ|t zE!cALxxH}*I$8uLK4B!B_rEHO-;7u7fJKxGoKV4kmgq_Z(Oqy&ORP5rW6m|wq@ zmpol*h7H7#Ob3&Ab8>PH!1^4?l_%b7+7~Npy|M;e>aDFU%aF)14zZrYJwVK{J;ley z#u`?_gcJDeNYDQ{wUk%;-|tn7SCxwV zKL-g6CRP9cI)xXLt+tR5^^6MLCQJkb1kt8{3r*}Mrlz$H8?p{MN=oR0&b$6^vjjb? z{T^-{GKD>GArlQ&THinaq5mFJg_J4gN14HE)BE!acMpe>J|Dx2gOx5SBrM8{)+)I; z%I58_RuG7}xw&?WW8n}lt8Qa|I1Zg~s#U`W>!ntLU~F_sVd7V>Ugg@j)8%kp727-R zO}#ppZ$RVz__JicNVRNr?{E3dVbhBJPL>x!MHjX1IR%*<%p zrPdZ!F~29_M(er2?Oz#QK3XW`g3ictvE&jaCbXfL6ikg>uTHL4{DQCp6X@hZ57PH% zs^u0MZ4y#a2Cf~Q>Q7?bDM|7sZy3`7wP_^}&yl0(n9Va9-&ErZJ(l|Qy` zw$_x>_rd+eix)XHHP~c)w*8kghH>x9H6`eia&vMZO-rt1S4T@xYkgr-xvHxtYkl&{ z$^%-4J|TpxAEutLDrk1bviiU&IvjUgx3U_xIKC`;n}u5b7q#@e!_Te?{Wij`hqI`K zIy53Sqd!H;+U@R~+dsd+L7Lr9X+JbtM`YNxqC`YQtT%ReUg9?4+^mIB*?$9!a^ft>E0Sm zQ~U5C(zq{lZHUV6Vm1vs(CfH^$ZEQx2ds|6!7OKTO3K=}*t2xGW@Yd6_Fezxk0j&z zxf<8Q`FIMa-+3}Od*#(NZpQ|KYE$^s_=!tW(m!h-iBh3Aw%lGOAd3Xth&gM zvt_Si97*Ia+!mM-ad8YSEiJb9V6#&|9dfl}VBw9XveGRZpm|*#&~RIfy>M;o!2Iyu z>*qI-48{~ibv->6wl@1USQv3|=q;y}#YMe5_0nO#=X*nkIdIN$_^1di-nWjk^Lp8z z_a0wqct73lNaw0fxp0%hZw$pTB+)CEU7mv{v*97H7#XqCVkkwvLf6(<v7(58+4_?<6@LKCcAsmbMjVgw$mHH|HzoS!FO zaQQym#8dt;sf{lvcv0uLH4;ymrY@HIE&yNAADx5?zEZbou+e7WWY#op?cw(P(?VmV z-0t*hPY|5wHYi{F-3c0LX=$U??g06>ZxOr0!X6*a$D41rGjOP=jvL3n@>s>Er-w@f zAx4Z=>9qtdwYl|utT*oVhxs&J@um4KGFt0?Ra=eEy{lfcJxqt!wd_zV#ZoMDZ~){1 zogY$AP++v!><}IoC)efs*gwa!mj0($W$5Yg-r?#%r=Y0Frzi%UxIbOl7mR`@eH_#c zFeCw>L;xHTqEF@Hk{<+SN!MKOC5#t`3o{!&J27Qf zx8PbF{!PA=e}P*&uJ3i&uschJM%g(({v_r!(`QOqPmrVRDNsnV2Yc4UajCk@0FhXVqZIKp zTIuu|9vzjcHt3k>^zoWKbUm1@g-c^Mlm$zE=?C;dX4fu(Ok*|>NdOP2{ZMH;bg+_IIb z8?IPLYdRQB1Hg^?aK3>Kis7ICmJ$4$$c5u1Mz`HLo7rwPx;@*1hunfCUra@#EOlG) zNn_D%j0P;00^rW$K+I>?jjPx6Ed1jLp{jBMLxIm|gDu z4aRlNE|F_a5)^gamAP-oIgx5MfV~1hl5c$+E+q*$1{Rw|$rOETsGEcMo z2Vl)%fH(@EQGbAz@HX(XyAg~XFrfkDxtXG6;IOc>J`fdbu2c8%*Vml+gX1S8FFR(j zeT(sLFF`5M0|bBZu1JZd@xb{u3~w{BuPH=4U;mpdMEktZ$VwvSD-2!U1jWM5 z$Lkfh`%2P=U}k0( zAA!e^2w(~))OdKmy6f%T#Xl*iF7WlD^g7^dI!AImL^-{X-`d(r06>@2+|1AAur57W zZ=sU-?c3W}3L(O&%h_fJIvDY@vomi42F1QGQuSndWscA(AQmE&=;>61PvL2Tu`_8q}1 z41@RYz|KZ4fI$sc zvFCuJl79YFYLYm3ry?Q)DnR5Lo}bN?8%0ouMN0N1kW5JcD{%9F%L6QFZ_m6qUdcu$ z<`7@}c(MCix=_6ZV|NAM3ZU@*6};VASk3}AgLZjHJbr$eZth}d0qZC{h3Kz^?x&-j!@SKm7T$?7l% zniB<52Vl`*uIzm|@@So{@l}Aq{YdB(bp1U&1wjkY1fN{5UoF@%1R-Le?ll3(^Jo`) z6@$m*_cr&&XsOlZW<84iWIMyo1e~z^n>T%d2xt%pZUlUs@EI~F!B8O7DhxVEUE3(~ z<>Cgxal9$r0H&pL7{j(Y|D$Eg5c3m-gez2EFFIkp{U#idCFB;lz@k$J59A5n`;^YR zJ!Y_|RCo+;@taxz(gH1p=6(K)&vwUqGeNG*kEXqw1O4OpH`b>%!!BQlKY&y+t}p12 zYS;9Xl#qADZzC2E{(OXyLx2fwFQ;vprtrW2hro=%XfD_K`nrg>p}NO z3bVu6rb0Am9smLkcx-F@V^$`dqjfb4hoa+MWpZdSLdYRm%lK&g3r~!H5r7hX_VMsJE#BVGDC?5XxYXA@- zDx^l6)8ojICWSRnbZ_?2id4V!U6jmpP*5(tpbTimZ(1@D{HRaN~l$asx@ zB)-&cup~%KN;*C6dTzDZI40fP*)at$n9gVSjX1C5_otI?I4pM&3&6FD_-kNgV}Mks z(5aWzy8io>292|{_yvi8W(Uf=7H4ckFg zmD}=ocL@h73zir9ham@kr|sy4&Hz0xttx|#$Q8e53UF0&A3l6__xa|1reM9&Nl^^U z#ZA+|z<>l(O5|hcq&w78t_RV{#To&{G-27`e)q4~0XVzr+BFmu6k@;tP>1DwFpFc? z_1HMRfvRX<1^iY)adA}8IZzKdUe`xAcl*^akmrYUthd>sF+iNRQpnLKc@SfRS^DY49gHrUX3R|)Ud`r$VnzK=p^&FQyc^D|P z1yg-JJ@Qt!W40$TsrhrSXk&={RrI0Z-7HgGf0z$kZ~_s zYb~8a*7!r%7L})Y3Pu1u+1_U;KrTSN5}SVODiEEn+?#L%D-O_-0<~^>x;_B8SsYkj zH^B<&{ht!qUT(eOTf2MT*%`=`v42_laDa-(+ba+#kSYe)uGVA#6>vSegM$OHpflsL z@4XG6{19iVfH=`RWWc7~R@EJ$Vox`4ZEnZ14i2?)0_c7-tCNLqAgj3}A8x-QYb?i_ z>zIA~*s5Rt?@uxCpg<8rAcX7&(Le;{I_->!`X&AP7$i-5+M@hkT24-iHo?8UKG+Y< zhf(jkwDnqwEsYMyYvnxso2zDD3;*t_4(8so%uMnow`H&x5C~Xxj6F~JLiJheMy6`K zLI?2ydPaOI;roO7-M{5&GcGbRGL*6eqjkH0?s@H&56+tbQ5fv&5D*f!MreU>LgYVD z-h)K7T3+3W8^athJFAUPL}Uu8#CGT($OJquc4?m4L=|cf=Ray`rxGL$_4R;D1j_Kl z|K38(ycB<~*`0g%Yx1+^>ZuAbD!wK3=wiM2SS zZax6?5C-Z#@#UM7J}j|jA7_Z@lp^Q{z~`JkgWn{52H%(4NA#64-MtHZV}9c}ZB zjExOK(QOvXjfR^W4;;&-976`Vr_Ow|-L}hV=Pj_Y5)aoPF#u+6<99WPSfU>9uQxjH zkEE=vS?HC&ZZ@$3>(&FjbKnyOY}2x5`AsaYcB}KN`9_$m2e!TaPC)IWq>- z%*<#yIy$?{9c^I@PydRuWsjFT;5sS8FezXmSUeJn8$thV=_DXeXi+w=g_3aqe?*Lq z=nW)lNlD2G+5Gj?I~8N-;$F>LIRF`v3sxP-uYMr)!hy(%TmX=4{BU!sU1J0TVkKay z7GuwUa*sn%8FVZWbl3U^2iHMGLF2EJ0U|?q5uxRBEp`xW2;Zy*LBBZR>(@w!jX?w# zbid%%Up(f3PQoK197Yiww~DiUdqLb4r^a5b4Ld-5A>PlA*ZE-c z{<6XD9}7;yhZ>)N0Kf>i&vbqVT0}s-z|tXs_zTS!Si2m_K3I`yATc?BfL;V?&F*{y zbEDnLk#Y0(_O=NK7l?T*@xhV80g|&Aw9lH>G0yrRVvq(8QF@`u=cRB=Fxnod{f zaC}j^+0Bo$AEI!BW-Yc0AjmWZ0j74BuLzW2`66!0nxF3WIf1Hx#$@_!t^{dlXAh9$ z0GP~|$VWhYactX!@L%!0SH7?g-4HD-E)F~Jy`i13 zVh8FgmEACw%(fW@M3+7OFmOgq3l6K@F#jveM7i+bseCRHh8m&D2&mc9mFK4!Is;*#mB6+zEPJ0TgS-M7#~dy+4UJ_94fJ1D zRIpUHh%G~AOok}lvx&&AEXl)3^|+m@hXXq;Pdsrc7H z0Z1RJz5!0GgN1?CVJx{oAXwOl1&{zD0+MoCU>6mo1V%zyNl8g{h!j|RXyUWEX|}n! zxjLv5v~G40<{P-^q0#%*ZO=g3F6f~>71rDb}OQD4o!za1*xg42g*bddbnS}+jic5tYO*b!`FtwTwfU0H8R|5Vsw*lyTb`| zzd_u4x}sVBe(`OkCP=F&(n0PDvj33iXmn8DvxlU?`=al!p_Up31oZa0Me3y-PTNXx z3e7llvNs)9KvwpHK$+pG4P^iP4(ssHL>XA7yn+IFz{|Y=f2mUx0hR(fC~pk=I$2`` ztX>tF1ay+2XmbLBhmARlY=VwZ9&_ML+(CR~4?ry*tyT3aAvjq%4UOboWz=9$fu;aF z`Td^SGiqn1NVq?ugJxfp9>BkAEnd#Z^`3^5W z*Q3RV83Qj25clNB;up?WXxG9*@eypX@MhN|=cfs3;isDoe)p4{uU z+fj`%*Z==_?Ba|X^Zz`P`M-^U%6$HUa9OXzlK?n-$_Ailw;H93eQzsRM zlxEd~<8SUC@+XnQ)Ste@)GSTg;jNX-{tFT=6LVwBSO1-P^8Eh{$m4gSi#Q!??CF$Iscu5Mvp+{``3B!$o{!@`aQoy zXgMuDq2D!W(iTtAduC3W7Vz88Mf2Q`p7e=|)V(=e?<9wz{E#~^9Lu!#<-gfqVZgiM zPz_^an$E{7UA6clij7F zA@Lg=d5~q@ElcUkPeqV-|I*RnzAF~U{?E)4BTMjH{t*~WFx7(ct#)eD4k9|SKk$DW zUC)oz%}W5lX;M6S&!JKWNUHd(tQg=~erAhN0rdu*n^F2boRU@-gYAJdH@*+Pxt`%x zw|{fY2fMP^sFt7xrhB0r(fZzV*1Iz0nht=k? za~esQAqVQ<;74J6)I+Opo2i0UaxZ0SZD;6}>3Zbe;hmn0z|IORhu@tvN*8>7LAfB} z`JSBnX z?(TB`KS2zT3W(JnASn6|sXTXSzFW;;0$S(;LIh~D1J||%P8(jb7q}>pDR&=p1HVU? z_KV3kl%r^8Rryql`K=eSclmB1LpN1X;*(*h;UibUl-@E>bws>L^Me%y3oB|5- z_iB&4rY*P{S{XVSN5{Guo5VzaTNrXHX&G`@=DnXVUW?D_wP3 z-^C%N5ry;o68-u$yzMUR`u@FPj!%qw#>CTfjT+PAOay)jw-g`)Xxj#3bD)d$>1t~; z^7;a52lhq?%%UL6K`uaF3WJ;q;t!@II8?qjG|*HB$d}xb5@cZJxtD=zi?6tzHVBbi ztIaf@``{r@99r7or1q8fdvy!lrhMV20-6?j_866J6{JzskM!=2$~aFxA23P_>KYCI z1Y9-ELxDL=6F(d^3Ba9!j{m-3X+|`@7+7ZUS^|r{A`zs|>Q?d_TxBlk^DCrmJ&W~% z+yzjp4)vHI-<1Oafdj|`Gx;50!SoHjdnREI=qC_*!a{C;fM)q-B^P?C!$+z2@AjVVoQcU|LP5zvxbV$ z_>7DwFgS;vEMVyMJK#vHfkr2cLI9RPMpH9&h|~3WnU!#g0(ku0CDgxnWfS|C6Q5oK z+uwjYU;NiHAvsyvf|t=aKS6x0<$9QYveCtiKwwYAs*MT)6 zUFx?Kq*9eM68E}?wa@AZh#ejSHeZ4f-nLKXDG;lFuU3QY(ael#AF6k!Q}j=v-ui_n z;ZX!-ba?K;YVJ7v?yylX$Zj3E1c?^NjswR{p%X^{+Gq=&0PdVQb@-FN-e6fq(mPkb zDU2TQW1u&g-)Ujwb#%9MfhoF-k==ixGmO*#$T9QJhdxJZ#lE$fKG`^PDrMz7pFl~6 zo+6PQTWJ`GMVPWz=RA#X@}P3ofAU8?+o6(%plm`k;B@IP@gy6pu1ZFYbrZ~}D_J*J zckgF~bWU-+69;b+#W$|IC##OE{DGDf8I$}Rx&q#pabq?*QSPq}(eM>t7ARE0gjSj< zamLd(l-2X9?eRec0V(lRpQ_j9ewIvkQohuEs~Iukx!i!s{^z9TXu57G=I_LFY6KG< zmDY^Es0gBtzMz_=&DwK~2Uff7mit&MZmbmtnNN`3CzEa+GN;fl@x`T&-_<=Xe(4nqk6gK&>_Z%8-u0{L}v6a=R z87g|sMrgOTf~TN{E4+%$TRV$-2#+c2mP>;j=}qRxAXIwU{#0K5=I~0o6P8;~KFroQ-E4!;Ac;}h&xrEEc;w|5PVpeT>c;NY zme0hqq3rUVhtVi_qGJ*|A;p}ROSU4mHylWX^quv{m5DX&>HRKQQ}W@-{>H>VDG1?m z!5sf{NwYYflvT}I*rT`Y(9KbTd|X1FPG%I%duawE#x<$u(Tri;3Ca+=IGN=Q9S&PV z^%-SUWD^I^f`SNE9G5;n2xR&oA3Xm->sQ{?yAqXb$x`X9lh!ob$f3%PeCiaActzre z8kIN%G5k9XNi+?GUsNn`*0(B%p1fLt<|O}#8|LpdP7kfq5nP$SKNvUSc(=X}V{iP| zjpOfM_B{la<~uvNu}_s*|JkFNY8&$6-DkEZVS`u-3ucbB4f2QAd=zSmgyeE3XY4ZW zR;DNr@ex=wJC)uO7J@M%?cYql>#J*7f;K;)IJ&qJv%dFgW{gRCDRfaYdqWhXdLf|r zgVx|z-th36I!=~#ZZPHk-=v934;%s{KUcOF6QGKT!SCmEaP7le-xy%~j&pW3fd4<`EF zx?}apy-S)XHxF)JiiRPLKG+qtU-p;|=Hn&YsNUGBX(P7KD6HH@rYu!`ldTn^K4|jS zr!@Uehs{?^Gd637iDUcGnuVAm%FcDK_LP{)5x4kbt+yr(;a1cc_&u_3e;Cc!hd)7Tgme*hm~N>?-kRc%ssBhd&LP?`&E!8stH*0^M5MUtqL`gfd%=fc+fmi;}WAC>NfRrFTq z6+(Hi3V`?%&ek(=K}Zx^u&bm zOb`f;k8Qw1b)2z47g4B~XrSkCM?C#&v>&&mco@csucp9#j+2`u*foAwC}%)jqD-H%x^lI*tc5yiLyaXvbG13Br&YS-MKA5zkH? zn_C|VRX1<%C^-m~j8>&U3c|c_Z$D0SScx0aWs-3(_Lmn2{N2Xe5#zrmNbIGr#5HB3}zP7o+EqS4J`_wzTSy>)TY%eo6U9 zi3c5pS>U)mHyO?DdEMF!%#FFi(D-2@)S}4d5;9N|hX!m<^QoQO*>`^owY_u-?~2=g z(=w?_|3wl5+&rp`n2}}lD}bwMLZth%bc0g7@m<63_%{JNGXy=h5n>1t#Cr zw=My*1Md3G`mE3kOGc?o6hk*7%wYU1E+T8o&cqR4WFEks>i=g;0%q0V2i=XHClRrbxTml?-GRD)E5bpc9X5HqO|u<)z0_XN zWAsaO+d54gIa{iSZh%q4c9g*8jwbKgEdA;U`Oh~hqfbt(w4TxBTI0U@IJO&&qEZ;I z6gvjejHGhzvq$R1t!hl~N${qhtI=g-kE@CSC!L^L#7b3J6GgIf8(06nTG5WDsO%k* z;)%RNFkg}7LYt=6RDXS*?ma5{z9E*isVwYg^>WMa9NymR@xm6wM$<+rwO_U!zBLrw zT#xEm*rKgL3+$lPTl#_=>$9a72l~7GeZ>xa3^+;Tk*B-aKia2V4BaiqbTOm;ui4ZAW+F`wOV=28+x7@@`DC3v1QndSMDg{h@nL0wBd`zKd|xdV)0@X({q~ z0tE@u_)u>UqdPkA81tlKM{1~{7?fY|uDni92|YFhqRF^7@1(H5Li{z954{Rt zIY;1n-ZRn}uDnJs-SgB8{~JAdoE_@$$0W`rvLZKpR|KaZ551Ktn;=`}^5l+3&EfS> z^s(zc3}nVlEngkc;b-yl8mblK_ka&@>K)J{iFH&`*m$tc))Z~h(qO&iA**k0W zMYdc#PSfg**qeigah$s4;jk3!cy#6WHMe)UqC#eV5gj`}()6l

=}3lA|Q=Rgqgc3dCM-#?|i-PVu@$OR`aw?@U2WxI~Q ziDmqEVdJJwt=`uYgVd64?JG%0G|ImM+g%Jf`=dN7_6SD@;27_m1rO%WArN|6(t^C` zF1rf&+CO4}t=BIc?g0k~I^~e>`z-M(Sd-#zKSl@6$oVrf2l`$*Jw!q1n$#zAOCVs`G)+ z>{n~A=PmY63)j#)292l#TxLqWqi@eGpQ13aV!|(X#S#<=Q4(rK{M6I2>DQGe2vN#hxuOOLuThJL zZ`#9UF$@tHf<;wUKBG!r zY>}rg@l$TNP6h4m1;B@S@N_9Y&Yg^AtfC`?w;bAVoFT_ON-Fqd*iSs3p!0VSLa@0n z_2sz*CB9Cf0tq`&=?&ZXz+JPIE%6%(FH49g&mt3O7D40FRNRN1``(drK~aUcgq*4V zo=;buGsBoT#NP^K)xX2ncm25jqKk_emmuCOsXaozCL7I!czVqm^13o;=#2(p7KBFt zlMW9hC`ELcM*{gU>V2*1h_;ySjA3FP6WA}l$%1Kvw2ZhXgu~xm)OG;!C4)<7X*VlY#}&ULl+3=|iT+3%@uJMJ``3vG_nsD<_5; zy0zlfSydblM}TfEoxCGC78^ePE6J<0+j(W^;v5RNA@DD zy0O2*P*XVRt+=YqDI$(jfpEvK$!2sn74p8o1Udio7!KP<)Kn)gASJlONnws2L}v>- zy6MeYDe~!k8ahLUzp5PjJKW*Rt9y;Ax|V25vuU)4 zma`{>Fd4j!aYa%@#~P_($#k=dI)q~_V$)l!`=-M)Z4=k%Q!1*dV7YO{>dCt2@rZ{- z1EI6uzr?WI@4K;PwK0Tbr(jR3ka+b*cv7Q~j0D=qAlL#I28=?6J1#=9MdJ9T!8i#M zUjmi!i<;GmcnW$Y3KC@s0wunDd5sc>Umgil;P{&Z(kD4xK~|xKdH!SE!Y%jafGzHE zv+|zSbQA;pAxTz@ChqseHt znk{m?zJA)Fv7i@%B&p|KpiM9`*~TA(Gby%&QfVv~lYIX!?Jca<(X75Q=*M$=b}aF9 zKZeWAW2%1pj}HrggI{>(ErLAl1`H&)KL6&t&3IG@!yl{m{pDBcqx=Iw!o&j28msaA zgJYR+a#6Y0ka9GJO?eNT42ZCZ14Q-VpMFi|5`xtg&SuvHQH?G{GTyLM3~^GcBbER6 zR-f~#MY8Wb+km0^5&5lf(o5_Yr(YNiURR#s0>UJUX|n|38XVSDO4>hlf6p! z-d$_VD(MB5Jc*t5S4xX9^L(3q^w(B67**}~t{sE`In=K1?~PGtYV{V6-?tPw-+F(d z7sk?&Wq|aMIhy@@z4M)823xUGgGcO>15PUM4+(@>-cy$p%m{Pb!B zwL-a<&RnqOEx5FS~X`hUe<_qj}d9m+tC*5V&Bk>_yhW;4U#`Mes}7={!f_x*u3$N)Sd^z zj6!j+bP3b*ZnNOKOxl3yK%DGPeQ(QbH#b^-%F_L>Jk3vs3QVyEn1THNsb59Vr3@WT zRe|ADnZL-p@rqa7&yVl-YW3)0DpKeNU$RVa{?h+l*gE2VHK~{Ubi(qYO^bT8mZi9E zGFBX!O1syg-K>J>KRefR8Q*k>rK9LY{H#NIa!|UY>VGC827%CeF77sE{Z{8TLO@Sl zwb(is5O^O-cavZH3w4JN7bKNC?pe$=t*>x+HAmdMkswBNc-TB2DT!Y_+r|%CNOJ!( zXPn27Uc1LzbfblD?2WPwO%{8>S-P&&Tu8a8a42dBW-Cr%pHGJ6#}^s3fDxp-IMOOQ*P1sdR zfa+xzMMB>ZoCQn3Qb7>w4)ITh#%@R7xZ-&uk&46jD<^vdWilxFRbUT_nBbRr ztyd!?Y-unSS{HNtzBaPk@s6r%r}5idL>{+L4^XnB2dL+@UFDAym{Z90+us78nwnrB zb0l2$jR}hjgIMEW!)PKW(?hf z`Lc^bjDtH*0)@Q7ja*b+*JVyKIC+_jC&rm?VYdgi*khQXm2I788@_N5QBR|ubpEDAemgVy zH~n4bNl5EoX|6Tz7e~fsJUWVSB!a`=Q)<^c5?pk(|JVtgw_#j4Wd&tKHrqp7WRT)h z68Fs$gt~cyB5M_CgEhYIuX}$Z&YZvesT&swx35~~E!sS(*JLBmL71s<{MU3)R$4;j zgf02;7Uw5JQ@7(*c)An;OE7k}fclf_WgEdA54*-$JBO-kCuVSckGJ=`iJ@e3S1IXn zvfNK%$kU5SjE}b8(z9RMEg?-a%ZdM#wQu!Kckyd|NMG&j3wRrfE2&y{V7z=j&Jo^W zl{@2RV_t^K5YhSXbMo`Kau&%YPY8(6`Phhb9EQ?bX5D^!>CNUdquI9%hZfs~pUZbQ z%_0-pGWG0}MV8|S@2&{KI#?>im}hm+|G1M7aV9J%J8Sh#mVshB%yCKyGxXTJlZW+4Bg8p?Rm~8l|n4ZX^jQGTe$d!3tqt;TDAub-d zklAtP_h3?oEdKr;og3cWL`^SJAuvQeen2|)lshbf01=X_*{04O7WwWIY)FR+F>&ia z^3T3xKjOoRF1Q18$r%m)*nLsXrVfOZ6DlIpdN~@&jXcsC%979u9B}h(jg%dGH`Q@R6d;b3QNEynR(b zm9i7!u-)&Q_AfmuG|Y>$(4SJQS5jAtm3a~_Iu8q*Cmx{qu>q!a|6sd?g())gZ#$k+PPbWRLP-JTeG z*u!FOHpgBR49G+8RHzyYo$+HHB(eFsr(t7av#7&W1nhtn&f}m{9UX*z0klJO0N8 zL*@u=bM_Wgh_{50n8FMM`;ZZ@U#7h2! z&(bh31gg#{qAC%#H`NL~bcNNcs>F$~{!OqUv>&H9 z4AGj+a6r4pW3g~WG?VwajKjndFdHe)@cQuO&_ZcKFnaUHjJuk2x%5DSSiHT7K+Y;g zX?YzXkD!=pz11F?jw6}$(g{^{sK(E%wWCdQ4zQNNKpED(A!hDE6TPn}_@}MZ5Tb4pY zSd6Xt-URPS73(XEM;Hzxe+N~>VO7A-mr9*wJHhW816W!xhV`lLc^H|<=qk8y_kc>= z?z@{Y7H#>v&s%%#jrjg~p@#zsX((qaaBjr8_3v6St>+ByRBj0hpTR9a-Po!^atNtuOaclO`~=z zb4Qo*aVvV46kEUyOxjUO9{e0{H@Fuj8dr{#RcDuS3hdz#Rx5AHmyz7yE&yJ;H`Naj zlH=Nrh+7a(wNyBZqH$Bfn84?XbNeIS$LtWNo~bVz<4-9BinHQy+7dsZKRI-FgVxaG76#-7~($}$O5M(t$$7ZQ_dIZKr~D63u#&M!Ws5%NAe=$tc zedZ6-^&eW5#o{&dDANMHce2i^G;j4U`y+sbO=kImW^6*koku=Uq^-w>8bd?C7D%FP zW$S@K09J@!$azI9CfH*i?rd`gU)O3$~HdmO9AlRF1~FQS+d5*{U2#yfQvm@9}F z(!K;4=4$Kp+~OGF^@}X2Jul0IuRxXf1Y2V8$&j%mLV`w*Z?y1P;(DXLW+yAuVPVE7 zS#3heV`)QY6_|BBei`(?c*ZYOvD)!62jGE{>rNr?$Q*6Hd?T&@oQ?STAsGD64Ll}w zZz|I7v+fUe>aC(q4nB+{Ng7?keI}lo4%;ja?(vfyq7nEId7rxFRY5gr@RC$b{Ic)* zhPLm@bFXk^M#O=ZVJ~Yt^IU5yrYu{}@Gh3r6wVWvW2l0;adt0Z%(KH%^6( zazC@E5FdI{2hFLU0tgu0t?;2O*?h@7{PkLVGycmuZy+BkdX&6a;!f2k+AONr#PxN5 z^jH4XLog?y$(|SaHJMLH(`ffbqECFN%j6-!a(;m?=m{W3qheu!Zg$v)Rh&&-NSFRX zMjf88+er&X?ClVfdiX~iG@j|jcN~YezkO)>B)_I2=#DwWpQ$IaWn=Y)x*4HMIjQz{B>wwz@T`iI@_*Cq zl;~H1&WUhUuZgaD-%#GdSFtPy`S+!4Q4GKQ8U{V;c2=!qnx&2ZiU^Lt~R>4 zRBqb(JZQ?_U$mjF_DDp7;%vnF#s=$h!pnH^;uVnbx$DEn;k$zL6*hmK?+Af4cGevn zejiSL3z^+D+tn1I@%#h4RO>Rjus~A1SY6rOMglJKbPo9E>;fd8B!B8jP``EIi zI*W8lU_XfF{k!1(qE8+eV95|NuE>=*DBkqId5!m%?$}a)@@8F#k#^_1X55^LpT;y=Nu;q3juCSVjSqnz zIwBSVKT=t4ef-%rO8?>&eV(jXU^1TywQ-MZLAyuZhh;2T+ATa?nDjWi&?Qyklg`vP zNNV3dk%}V|d%&;nc%y0l6Dk$ z%~DU#B>B+xU3~vk>#9S_ou6rElWNjTqEh$mt4Ij*k^SKZTvV zoQkIm`sc*LbC)`!rU(T5ecYwJodHjBlyl#;`%ztngojCkez6O<7qP8GSBd3pk1^C; z$<@MXh7Oe*(qwb*fG)<4omz(^pfv&l7Z}$ixbZCu%$!H>snv;d6ea)^)zWHEa4BnX|Z4USH~)gRgj0r4+?+lOls}yCm1$4KBpG5c`g)dNngKHpU2s z{anI7NxuT2V*k*zMC$VI|BJ4(imR&ozW$*bX^?K|Zjd@O(w&mhEl78FgMhTs4bq*0 z2q+~WUD6?4@8b9TZ(lF?2sd!n-h1u2<{aZYHeQ<^S-Az*IDnfE6SRaQ5z{OM5DgZSvHXyF|O_)k!>6UEkN9U}%490=qa z6A4kp6?HH;IH`jB6l`zGPTH&$uB|?fFGm(%1oM>9nec|_j@NGIg|Xft6Up_ATm^C1 z7ik_buY4Obzr>u?y2mbVvsCWRczpoL)m(|1GqmK&(6kHQ8@0kuWsNMyP+sL;p~` zdO7}I<`5@sq7)tVZTN7(-KpNZt4d$XgIy`&#J_HHEG$WrrGF?2WKmaXv%$No+LLrQ+4`5UR50nJ;V+`y*So5e$ z0sJ%?l37cL;(v(M@gw}hwpheU<4A7;C%S09H@0GO#PD=ju}afiTzg8zwNOm)i$+a1 zD~oR(LUDp0SJ8eLV*XKKFIYXSGoIt#6Oh9(2#y+<7ITq~-LEw2T=eUAS;}2ZDBPBL8vxNbiwf>w}5a(d1{DcXD9;!KMt; z6TzC{;K@!q+?0V|aMDR)x^zni37jdW+zTK6vrMtvrNb5BoqsWEnka6*+aF6A@Pmg7 zI;u7Mr_?Awm zMji9v3@VGIE_km5tAdlmx)C1{G|&a_o7FaJDBMjEHgW%+UlB1bGn!puYq?DWU#DkZ`cgE0prPlJxi`?!#y;^ki^pSz!NuPAs+>yU z@i}^hbyHriVSgx*+Jof1*cmy441UP&{?o>XywRcL+0g)Tgd{{KiWhv;#8dpQ$u3)5 zT)2uDNWQ@BCq5af5czxIu6Att)#A{0w~&-MkwSlJ1hhA?Vs~&v&f*v+C^=|!g}m5L zp=}E8Q0lJDJGNkRl50{~gWemraFVkY-uRqWZ<^)^)~YO}Zy=NFAE5_4I@{vtW^DeH zl^KGprrn7yXcB7*zahZvCf~*=tNS!FS#06$IG{kOEe|A{-P07?8o=yaPw+bFe#yg{zH9fx7h{fM0Vs@s z7_LWCSbobwCXfLPU!kVaQ^NVOyH%g<#8Ea*q(vU_OfZuE@8J>1<}a>S-%3;rIk)w! zawYqcNGFM`(Cr5gW;-yD)IUSi@AiZfhvt5KkLb{H;)E{-hXE}|EB0}GD=KNpUDk7N zhl;g7lchPR4!37L=cZB#sZ0du`;WFK+f8MAAJDwiFAK-0FiKET&pxg6rTgGOu>AUA zYxm{@FL4nw-?BQ=Eu(28UkL;79>*cxUeYcJ%OepCye*Y3eX}3(4G{;_g zmK*8dM(J667WcRx-Kw4;N&HxAC!u+W?Dqlp9hA9MGV2$%I!OHyyOXZ zSW6#ra0J}5^s)eP3D6pdGuVtI6%1P*i!_leWW$SNV_|Lr$%d2`_XEWgy9u+FT_Y;&)r~UL`!CV?L5bO;SF&fx|B-drbBmxcN99b)osaiaDrX7R2Cr4*Ac$EtXbq!p^K34J(i8FNLl zAORASSkAKA{bH6IY&4%AoRcrc+V`!=8XtTJ`{r7Wcsc*s`yU@~@RUqkHqO#J6Q9%r z0vm#U5nlf(;Yve$#K}hZLKDz3bB}F8Z3enYT+<=Pi2Vv!g7(^JS5jcq{w9L2aD8<` zE;$<_oXSmZ4d7g(J~#m zhL&Azp)bQDqf&qJC-0?6t+<`pvk6U^O1z1s)lT%@qPan{b4J|Qcfk|Qe^0K7jwjw) z`p*yh6cwhj$hfOYUhUlhS%whGDR*}$PnDne@t1d}>UgQYNsQuC1B=gio!_7`3B{rB z-g;^!SrS>(cN1ue=86=>TsL2guGK2mBL!>N7rMMuhvZxH2iD-CP;SkHn2ofGNkOAc z17!*=b`_LRAJscFtjNm%l;HjTEJnl&3Yk1nKUF}=0KB%fo**~~Bzrvk*;osV=m3d= z^lWnccl3t@_&S^Fy9!b&8jA8`pVoD95ipR4#wvOH!$mCQuYQe9)@=9J(z<+<^YnC| z8Fmj7jnu4Ai4jzw{?CB=oePXyNjo94@V> z7RHzJZDjdVHf=R7NbCtGDgJvU#`6>@kTA3~{6suArrao+p5r)^2RGr|&Vm3*8-AMq6;)^>$6``{t2qp92L+7pJm7*NIum29Nn61m$ zdd0fC{|5zX>a=CCW*XtrNx?Y&Q340Xu-|)6%c)w<^PZ^`Cvp4bh{}&Ji!->8z}=|? z@!wujJ;xXle=gKrHqZ{?NK=B-Q2Kto{rZ=CqGO)&Tmcdo7aKB=)U5j!{#A|{ZX9#t zi@-&JU^VZWnr)2rL@}$)Jw%OH9J-pJL~&~`_uai06Q%hv3c4<=_J_!6Oz|>(D*>H!C$c%`8%d~m@@9%t zfo0<&jEa|PyL0EJBA{Qa>XlT03D2L9nl~w`qY39(0B6S8+vubuekWiBnv~o@tWW5X zpHY*rv|+u@k6@ZnAQm3CugjLupG2B4P??sz2}oSFd(DMn$n-Rh9^ z$AdJgxJ)oB9ZaIJFM7I!aBJsQ1{Ie;a0m&fqj2_ zE_3-u9csI6yqr=yWGf`jb7Q41i9>Wd7nh6I@*tHT3TXD02Fn- zjl+8A+EYO_-R7O|A+sIaypGx6{-dbhB<_3HFkR+~)z1;>?D@Hp@I?y-0dxEV>nL*t z(h7#2enjn8I=lvHH7?ON!zLI@SXj_IO6=#8v6y#{`{$cGGVdi0X@0Li@y!^>?TDUR55HI&;?@>g2ip zNOW(1NDjKNuS;N#UfoB6&NeJ%pwto1kZqz4HL^00A@+Q{C;7e>+TuA*QX)HQBjR*F1RNS-$>K(lNC#xFqX^p%(frbxg=Y≦%mn}? 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 literal 0 HcmV?d00001 From a870b4b3faa05ec4744d5e4f20b526dee70a01a4 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Thu, 6 Aug 2026 08:12:49 +0300 Subject: [PATCH 43/52] Add the two-host socket-grid charts Throughput per socket count and the two-socket point over its run, DWB with the replay warm pool against vanilla, primary and standby on separate hosts across a 100 GbE link. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013tqXDnyVLb628yQ14HaznA --- bench/README.md | 6 ++++++ bench/repl2-time-lat.png | Bin 0 -> 73315 bytes bench/repl2-time-tps.png | Bin 0 -> 72557 bytes bench/repl2-tps-sockets.png | Bin 0 -> 37347 bytes 4 files changed, 6 insertions(+) create mode 100644 bench/repl2-time-lat.png create mode 100644 bench/repl2-time-tps.png create mode 100644 bench/repl2-tps-sockets.png diff --git a/bench/README.md b/bench/README.md index 9f199a6152d5f..65df394e1d0c7 100644 --- a/bench/README.md +++ b/bench/README.md @@ -18,3 +18,9 @@ and data checksums enabled, converted from the same reference cluster. the run at 750 connections against a synchronous standby, vanilla against DWB with the replay warm pool (`replay_warm_workers = 12`). The vanilla run is 900 s, the pool run 600 s. +* `repl2-tps-sockets.png`, `repl2-time-tps.png`, `repl2-time-lat.png` — + the two-host grid: the primary owns 1, 2 or 4 whole NUMA sockets of a + 240-thread host, the synchronous standby has a second host to itself, + and the link between them is a real 100 GbE hop (RTT 0.126 ms). The + bar chart is throughput per socket count; the two line charts follow + the two-socket point over its 600 s run. diff --git a/bench/repl2-time-lat.png b/bench/repl2-time-lat.png new file mode 100644 index 0000000000000000000000000000000000000000..6e5a77c65c07d74f4a2a6fc1dba1cf7dfc0cc2e2 GIT binary patch literal 73315 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 BC8_`b literal 0 HcmV?d00001 diff --git a/bench/repl2-time-tps.png b/bench/repl2-time-tps.png new file mode 100644 index 0000000000000000000000000000000000000000..83527b09ac76af50200f2387f70c8b2a92a042da GIT binary patch literal 72557 zcma&Nby!qw*FHRSr*t=nq$nZXh_tj4Lw9#~HwvhPgp!gX-3`*+9nv9P-!=F9y!ZP% zp1;07_QAo-o;`bC*IMT~&vUJbR8f+}LMKCqKpw2RAndHye$Gi;IKvD^5<^f6w5s zcQWU6nnSn(S3z@-({YACaG*3V2OhI>X2__8RK8M1!lzGv4$M%}b8_fx=6<5R%~z7v*4F;|WnCkV zQZS6ie29jF1KaoZLRv~n>gyLoNblfaR|@+(hs#BGNsd&MdI8r;r%m}Z1*X)!{rP75 zWs!6t&oRp{^L7nbXlQ6k1D8k3@n_q^^o)#1!6-OkQWByK)z{bCS$Ndc@elX6DD_XV zOAK15^y*#2=YQE!rewWxMZdi`kTf%6x;fv|+J2aJUcSvyo-Wmk4n#nqV`m?ajbPJX@WW+i61%s^j6o-`?KN<$YljLm`0A{_dBQr>DR# zyTz!(#RlW??}`x-5n`^cFAwXssqQZ3?MXy@`042Aj&JtLBw0VMMRPWb)z~ekR?Kyot@o&3Z4XGI=7BQ&HG}%tpi6CvsyP6+7U)8C`dj#JF8vq^7OmH zC;XkQ)s4R7G+}Sv?s)2D21%>+?pQfFxdQiZg@h>l?@mzG*4BJZ$%8WL zZ;oU6mc)*hnu7ZiZnm@hxvZzyOw7$O24IgGib0%HRD=%h%Hy~VMQ!vtd#1gERv9WLg6B&CmH-W%?!ywT?;y%IW;#kc9KSX^a7-ED=SM=cJ!R{Y7X) z5~lUNV|z`WCw=_5S$@>=`0xgs^3}$G*=?GL)FvzJSYs(9Zk%l_r3FeWuZq2Tm+1I{J%Cx_*;aB6FrkyI`ynLadDB zhLgZ29`m@4Q}9~Qf=f}rQ+%7DJpHqj?1g0#6x*CgOpjRHthrJjO^Ev*H4>NVn%n%x|sjMH-59Vv!XG&$`sqCoF+Cwq; z9abQnM%*`-NAX1Lx)6xVE6S-7o#4@2S&RV>zsGxbTnUG>K|YLusHmuN^I@uqiHWbu zSt8iBl{k2UZu=yam6ZvJiGd=%H~bVe1qG;J{hmP#2S2}zh>Mc}fqU3^rW6t$o@>zJ z7i1nHx~1p77z%S$=Z9sJRu$ftb{5sTCT3>Cnj4i2#AS| zhckt_$YOVvTl||=`E-8}iQXL}ymH%*fI|Sd%|?WAakv-_J|O;04SIFFn$TwgspZN} zO2SVE*=pc(f&?+#=uMPURz`tPi1?5I=#hN?o&w}=`0?@a4d-X_5}hh!LCOSkg0C02T9UC9#Hf)7NNd5Y` zwYzHt9uA?atIJ}xk`Wn;46#C+t>p1fepW(CN*I8>wUxHuf`S6EED_%j>#aX-V)woi zDh+#k9lw7oboTTJQuMrQ!8yKr;0K!kurRc&`KBLwy)&A-w;11(+-(25>D{u(1#~$v zF>z<6g8r5J;eNTO|AB6`RcJ>zPVUd21i{Ey646BLhA+q}oVS$-2nn;nlI+Y?7Z3Q3 zkB>WE9YZZ%kYSQOM|}G9Db}y~jNAO?Wuhlff?-(rCWWo7H4uSZh&ptQ7~Zx|mF2_t z^+?$$TnU+2GHLK-3aGRu=ev`L5D@*?vsLCeg@uJDI}7om5B!M(n;TQ_>=|=qV(HWh zKQvy40_I%yUOy+ zOPd(7mr*WzQ?UymC`=aXU6l?5{2zqsHdEfALY~{sy?~|e>3-EPz69!QwKXxEn3Ize zMcizQzyDZ`y@AtqhI>+48p&9(7U${K7yBg5AG(QxM~%rKmXanW)S+Rsi@YdNu6t8> z5G*eO_k%fUfT_J-#e;kQ{uzHAPbJ)ITeG-Tt|)o`29@^l{>VS_JqI%aGA2zjvoe$I z+)vsppHnp2&RKfb>r>OQI%o6eUVkmrR8_@MS8v#Oc#;_y858@GnUg_^CdI^HCVlzx z26a`4$kEY}0lfSB^UG-1LGj8Z0u&>s0rr}NkPyV@uDj~b&3==NHT!y3I&Tj#8FV!J z-Xec4ym@9sp_X?NmjzU_q7OE|ISyVEz}n*ii#A-$z~4ZFhb znwms?zkf6I_4SzmST^ZRpfza?fRieo1j{AhaZK}CL4ko%(2W81G~lE>0O<^(q6Yjj zppgBUui>;@tbh6Gg(W^Iw-I7upNYxATs8K#lY2whO+mKX^kjGlzc**&`7N2Zf8*6! zbe`fD-gCRRsj~fx&GriO3$x~ZNsR618Q=nuF)Em$Y87vT)d%*kj^m=Zue1sHhf(lT*4 z9(G9WUe-fw;3NJ)o{^g@)>WSwy(bTaM-+>69A=mS+gsVk5rFh;!MWNrW9~gIiuTlpyXuy>cz;s053M z=HD>ha>rY&RYvrSn*_66? z4*#l39x&w{j)>ff*YNop7$3#s(aZ3s4-mSNsvTbV8qZCo`JSGgfq;w&&QAXH=`}t+ zz8n;qU2b!{(gyaY92#7{zv#Zq2pElo=<%)mH#MibD@W)o24|yKPEHO*Cfu!v>-K}= zWeo`E7>LCwk5aY=28@~x283Z2);Vt{nGU2Rf*=fcIhgy{+1;H4N-n=`*{DS?cvf*H z#m`cZ1dvrVOJ_=7cw;e1xLPee0dj^=l-%9<&=E!Vfj5~;sCjKj07x|+Kf=QI?_U>Y z1WT~)8`K)-9{C-71|0*UVH4}zRU_~VoXZuJE~vw zDy$}1D{W?<$3Gm*ROGfT|E}?E4?&YER8R7Mysv3HIBL4ohoM$dM#h0tliCd>dQLln#K^#dgVJ^&Qz zY3MZywT~DBek9l1EI_zFET6B(i{ghtBiamcQ|hWR1Yxxb&2c#4bnD1^{~pw!)eY$~ zMZK^O4Zh21zv>#4?d&*ae_B4dJ{{oft27;u1B=s_tSs`h#NG8E)9=PIN3D<|Zmp@%eZIE?w z`FUI;#}Fh%=ctVgdHxAgLqj9hs!BK z_ClSr#35$uW+t9QLPmyOzLN>yCnvM!S*w#;23^MS!eM^*H&DQkJOCi1J8>?An(Fuf zCQc&g#-`5taokHJKRO>^Pnv);lgPuBF|I@_7p9Ss5d^Zixrsf0`nWyau-5VOwEOobXFj4rnx3AA}#vIUTAZ*X6+=tQy zU||VXvBGwqm+gHmE(#8n3qHC->W_Cdbex>S^ofxpBPt}ZGTLlJat!#O5YoMRMFvW` z)65~boFPf~)9Yob2OTyxHWvX)@MNd~dk7Eq5097~wJ#n%NBxs^i(T_AW!#st5)$z8 z^73Spe#$q^+_uH`S<%FtMki}sGnJ+ocJ+Ulpjk^z-odzI1!g zJ&WcG8GCJ)(Z!!_3FK*D+TSVT45-K)K;dh*GrdDh&8kTK@6U-}0qD2K>r14UHF~(a zuJyTouKUXd4FZ#RKy1YViKRGW2S{#2baV%(vE@Kl!?+qYPxBKdh1RvM=o3KcU$Lxz zQefKH-1NNr;-OIj$Y1=WE3th|-P6Xq(*f;D6SU^rL%|qI!Ks7$p$x(Bi-UPdrsQmJ zFFT73e9c#D7!<-@1d9#s?#2E+vE<=B)czE}4R9f0(64jq5?|!x;d#>Fe$?HYNY8Dz zzz^&LY+71c5`G7owe@vwpDTyonZongzT3lD#6lE4gFN0~d+^A~qZ1Nv!TK9*{r<%7 zx=X-ozr<$aeDl{527ZO#we?E0DdK?!eI5Q$;~6$& zi2QnxZ}}^bZYr!Fu^wcU9rNSI#>RGSYt#x|X2YvbwvL>)9~`6s&o}`}x!#w`d*R@= z`{$2rUl00@r7!OLGu*IsAtfb^y%8?GI;dIxI%FPE*x-8DB#%Vhk^jXVCn}5)9zp# z5mbPNjBip5L3XBbL4(RO)LFY=5)ag$-1_=C%U}K%`?E0l>2A~Sg5qOw^z)^M<~<#P+-AjTrnalsy)Mf>CY%PS^K=L9osXl(#~gPjX@xZ zzm}BzrgbqJ`6_;n{Wa77!Bbsb-4HNW*hhdW2m%Bd$~&L&kO8s`<{1D&Xav3nwy@|)7fbV0P`Gs7i45Spt(l76NSA4 z1Hm%6Z3s9*0U#P`g}w{pM2W|uY8URA$37~9kK;^0E>P7)(YzWAh#r!XJV7$;I z_mzW6B6Pix-0la?5(k`m0iCbGSY0s3VK`SLOc@LA`U@R?n9Q z&ci?uF4T#>cU+I<%K=ZX4NR_%)3GtN)!ENNp0Mf>2^=oy7+fKmQXz?RqWgmvBz~%I4nl1&ong)n#M&L zV)lDU7}f#0*9j~PO3ePf3;-m?FjopNw|l=sSjNM2{sXw6Omv_M%TgFH-hiz=X7gvV zm>ATzW--2Cr&w;_M<`?p&AxsSgwKzTfdQjys`OF#>MHU7Yps71*Y~?N1%?LJ6J{k$ zm$xh*zQPdsUpIY?_QlWm|G)NmHj<|=;y`8Z@USfK;7G#IT%7;);pGu81_M}5SA5ikh8794&b0r zU_Sk!8GY1ziwy!6zXZ5&PfPf|48nYC0(7Lq`aKStrAAT^3SA&Rai2U{KRd_5qoTqN z*b9k>K;_gI7@F63|32pG>I#|NRQ_1`Bj4$zEw#ZS+X@FSgh{6Wc17j(zIzP?7qz~}&ZFjDxInN6>r0POkS>zlBZj%#51 z_!!CkIrjn`gArm`o`4JUv!MSv=w@!lGbYkoYbxQW{O=rvu2zDtuGZsK-@c`1Tdpuh z3B#t0Dlg}P1jsS8gXAhV?!j&;$^Wkj*q{59Y@f?1*hDd>wz+t zHHh(FN3`J|DiQaXPiU?OXM%vp$#7-~@}7<-DN7L5ZY7oDgm zH94Oh!N#a>Q!}qq9fecff@Pf?!)f1OD_6{LoZ^Y-Pth;L|9!NLWM!rcehr%d#XWm^ zPgPw#1X!!+Fb>%n`GgzjLg2^!-J8Z?*Zm2Bz_6*xe3*z~{1!Oq`I%nmpty;*_}x2z zdIIa#fVRllfY-lS>~!CbD8DShcE-M8K)%-3lfiZybg_WL23ij3 zagJNRNnywgYxtDuH{CTia{viUM^6uo^VQ@Lk!oG6kMh#yhM(d7`x#*Ww~b|G#;hV( z$-OiOEjmZy|5{Jj`oh*!x%>EUY3{#~2*;>w+$}0#wqf}@&@O5o@y86}zpX#Zhmx7M zfpx``G>Ev|sTC&m=sYMg$n`o{62kw7Q;ak=fgA$1KV5ocyf$>Qv zzrr6%>@cWNcyvU+lp|BbK63=J-pp|;mu94> z{e#h#zkI5c_kPAG^XJ2~mu-cSG7^>E528k8A<6mhl_tUUQSR4NXu!HT zawbmooJhYtN|-RvZtP{(4ZQ~cyWZE7(^SU7<&~%cjBT933fU_B97T#sG6WD4hQ7}q z@sR%0$Nj*h>V5H%vDLV^EY>KgEg+Q9;~-THB37an;%{IR$$qAG{8v#G63~v0(e0NO z9)#`;dG|V7W#L<_eAL|5ST&vb;nV+geZQgAe)g@#r70dENki5%_Hi%A*O5*iy`I=1 zagqA~mAh48BO{J~B&WoJ^giWYy@)?m`R=Ue90s@}PT{DUi#2p0us&tuw{YlxO6H3| zUe>2l961^(M@10Y7cKcB3PeGCe(N7@@aa0!cqUYLeMW1wf0GfwLHIw~cZS5DseUuR zUs3{{XwoiSyD?)!a-^Zg&r3D~B>z5b0vUVPvvs446np-nj^dUNbu;U8Oj^e6aV1t| zAsnT1`OGYn}4GvGv4^mo9O^cTX()XseQ$;;zZ(c2hTX?~J}CObIh2z9uQj z%m{(AC4dCP%DSdC=whYF=e}hT@5z3XPRmFHC#?G--b>Y09F~pn0sVY$Srprr5~6U3 zbAQN(QB%o>y>D-Ahv!KlT$$03guI(^*~Oo%RoSR3kcmy~@~?4ND)_67cKGLLj@(qa zKp}*hT1jSF-Oli0P*hjwmu_rJm5a-1xC9yFKv!_A=j;-o$+dI2@%s*P)oZTe$iv z-+MVZ-9eW%PD=&9O-Px}ZDx+0LW}40FcB6OIVqlahd~YBA%Vq=P?-?^z1nmLrc1mj z%2_X@S)_InXfPbCeQ?fU(<%^$&y>SfQt+P63?R3qtI=%L#{$SxwkqGJO%l_pRtGXL zz5HsM&9C-8o)*agf9ctwa_8XVKv(t~WS@pId{tVbg*lBs$=(CRmj{1*=!-DZPkOdR zT$JCg?A<1FU61=rq@f;Oy`UD%RFWLfp5A_6*+lW#suX#my`loQuE=B`82}pCxOR}C z<>l!p_qmp(lIKOSY;guJ4L7{&l#EFGR9#4{pYU-YD%R|2wJ)eQCyDa!2V97J{+bIwzIuPAsBwSG zTPO_b|Jv{3KQqrck<9fK%XpTbsxwId{L+4|7dodJg5k%SeO>_eJNrk)iVZEF-HtD1 z$T=L`i@ehJbwQx%xw>4WQM|hVrWTrDJ!&F0Z5p-#m}x3XPme$^6XP&*1V$8Kg1+8$ zFAP|#l)y3lyX%GiO`wfjjn6bJ2nvx1sQGc)IZe-Dy@e>&H=?EyRn(FyS!Ki_R?^J@ z&Az_jP8)FATmH00j7rP$D@w65eDY{jCG^el>>_Kp=~82jkLU8?a48%_DvUpTU+BdP z^!P;*Bnbao!_F8}B(BetSRWp3$HlCDhZ|kqmAvK9F_w!>F_QDXJWV&lj_LfPrE9c_ ziFA|ajKAALwY{sVXAMmukmqu`s)W1CbrIf>g*|UlQc^jz1KDP#9PZ`Imm?N0IBJT4 z!vq`+NTgIyP!K${uFX&k^kjk8#!Rsd%nafw>uOz0a$Zc()w%**?2&3~Elg?=3NQep zucMs(f*I5^GHZO#Wli*$8m5>sVeQEDq&=SSR?|TW&TVz@%-Nz19c?s5=Nbh9CxZPg z{#4S@H@SxT7$5x4NL1cwC04hVd_T%H6e~7I%FQkv5%Fd#*)v_Lg{{YF9ggo9A%~>f zQW^=f$ESz>qBdh|8)SLMZ=_jwde{NpJqsU5=Qb{0gnh6;L#|0QT3;}6c~do+t6SUr zg66=HSeGrsiM_RjK44F>0teMF?t?n3w2VyHvAdqo*>9fO^*BL?!v%rVJwcZpNMz@= zd5L}#8BsJ6&^7|FVH7ZE0UR{rKV#oKK%4$^W2S_q(oFa{NL7!mEs_#ZH6#p-#xrkH zk<3Kg)+r=pE%`9IZg2Updc9{~b*@>qO^u`y+?zAeF`}un}amSnaYHGiFo?HK4u@7r%d2W4{~+nt*E~*^)3z{kxiNcIq1nfE^XcUi-JiAqT1+hcxcHmhxMH%<$twnKj3O4p` zgjI}9PNt5?8qZ_}B3u)$M}K-O2-37$Z#(3RMzh-J2i0<^2Ky#aJnfbpUpv{^HClb} zI+;pCK4N#5!z8Aq3lW!&Js9$}^%;-%rJ2_h1huqr$oMHk>m?48(UqWQt(sYPj^a12 zG&XIka~UkF;?@K=*CWtGyya1P+TLCFn&eua3Z0cryEdC(G@Gt$NmLO?7B|u&0Xllb zr=#M+A!itE$M<;-xJGZ!_cKZ@)U*C0dwtQXb=l2^Gh?f*rzM!2Q;}%pC>RT&5Ol)?<1QdW<>VaFrQ>5`rFBSC&qgQCt92SZxWS0j#?MAzuX_33JC$d) zSF*9PvWlSw%1NC|l)46Gi*gdrnoa0PGEP_tn=M1`-0-<{0{D0d4M)@VIuwVij9OGl zHoBh$8c{lIL{X(WMdPv-d-FCmKQNRaZS1?1&o9-Gs$NLt2mRs4SJ(?Q)IEn&l@h}d z!+}(M!WCU4PO+zkKUHq^@Z*e9Izo9e-TzKhz=CC@vR1vHPFOP}5A~32HwG~ugPmWa zbxl1Ri5WvXU8}0RC5jD>%HVCxVUZQI<@?&unJ4YAnOvkIp-OQvnb2X{uh#wsg4*qO zhqo$k^^jZu-Fuqlw9&Xhome%?VvRHtZE5aT2SLC z*~_a0esk?>TTArnTOP#L)bWk9`W!Msua%0IUAUi~Ilh%~P@tgf=Wf896!Ou>vh*6k zUcHmy@)3PXQMXyLl5s?09;Ex@;)(<)){h@Q+S!x;R9*$G;7FLG2u95~rUy)()4t#W zB>@2iTS`U-c?t|q!lX86kF1*N1s z+})y0b+|BrDdTaoW`T?->91c>0@pgopU)^I`3=2`g^;V#SmakmVcVtbZx2r&Onmy_ zi;t_Wyjh#4DY{*+kGn(o72at^up*T2S)m>t8eH-m6p8Bi5*A(hAS`BO1+bUWmqp^y zuGWdt#=S0VP-P`ls1lJrDMo;s!u(Sjk9W2*9s3a5*4y6%=*lIheBE1&V=Qk8+JzMs95=gk)Euf&Dl0Zok=s zTLrYZe}t@9vGy@y{$&dC$H+@_H6D-imCwL|>?E*AH>6;N-xR60hL1mUT-J&UF5J9T z@UT*FYHfU4dh_-hyq}j;{Cvtr?wlOg5i`P+D1I>QL6bAi%g`k?9>5!WxKxmLCY3fX ze5caU!j70jje9=ILta5zkB1yp3Wc(Kuu>k>Sr+WQM?^w_#R7~mM8Ah?bBGvl0zvN! z45T=mZoJMvkfy@})14}+ZGDZvK*kfj|C`*w3q}=WKwG`3@_$Vk(*_?$9xlxFD&x0z z%3p7*`js|?=%mUu`ahg{Y!;fx2iO;|A>7fDca_9Ce*8YZg*;rjSS>_TLgS#@L+C*@ z*H>HAu%GK@rY$oXfot-7Qn-y`UkQtvMNORdmS+Vi@VSG%Q5^afQJntrC+-Q=qVU5- z=4`(T(i1_D9(EI3<)Kum-t=vYz%SONx4LPN^SY&2+kTActQn#^{KGDHcSOp9GVIde zoA^W@xpRT0daZ}}ORP)Pvn~P1PBKSNm9bH|nG*g`^XIr?`|9rRJ#UX&3R!)GI!4D& zhbT|{h|^a(bm%0d65a^kmMHDuaojbnT6E9`o;&VTQh9DJ@Vv@4Ckb<3p<^li@aaol zv*)i7mfn=*l|X!A&LU`hjnqq@wjBk==2|{H%nr2Ug$pQ^=D|4f`=|Em%sp}>KQgxv zRD)gCqs z2O$|c3|O7ujlaOr^ym`s?xZuE+-GR?dB@i6iX$1VT^MX`y45P@mh}3*47rC~ z?U3bS^1F4B0vFrVnQyeL52_2bEvYGko5gZ#WXdw5@hGxZUwn;^!z70an{j{>9v1IM* zyZIL7#ZP-MVXV|o0+x=W#urRPQHW&1+G)V{1$}PA=byf+3H10cI}Z!jo(%~mg63Oi zr&Rvk_wS|oDxWbfPPbQSu#%8+oo7l}IUBaqjYRYQL;=@5vf2Yn9%!`;$Qq zB!-;BYgDMDxo11;67Z(FBW60@_ zcR5XaIr)sQOVJm$%`JD5in__wFgpxkfN~p-eCzzTcQTF4wd{#R`2(uC9F|3{m|7kp z+Nx1!Xr!u|X`6w0F}JnKdg;J}O{r)*A)N_>7m*sTvmt|w8@jYx1=^2w1bXG8r!}r` z*B+Z|p0b|Gdp`K!%)ZqaK+yPZd@?<26*HuM)5J~?^(=0i%&>fFtU(|B5c*_WQ$J;n#LGNY-huk-sKB;>uY2D z-hDN(Bh;#)gam1<%|_hyI<0CXG_!!B`ITdcCb zFRS#ZjI#CsU+MBqx7qNEbER#mAH0;~ZH75@Y9Cfs5+)O(G@^odQO30XF+foT)K<~A zwcxD_!VZs2uZld_Qqr;{tfxZTfL>6owsR5$E{=yoK@|#Jac{_%r2%Qbx+?B(^VRU7 z!zN!h4!Bu-m=J(IBlCU8*tSy9Zo`Vo|BL3X%Sp&n+r#SHEGip+w%ih3X?lr3#6y!v ze~~yT=W)g+Ri#M}J+Ukc5u6H_E0yZ31wXh-8GBZ69|HG$yVLGxS_5mWz2w&<;SSHd56MZ;!pc2{?yx2N{1b2QX%i!|9(g3ZV#E2F zkaDK(cgs*Vr9T9lNhFP6LsYH@{M$59IG#2azGvrYGdk$+{%tix_IvS}2J+h1#+^6v zu0+|*ABz&ReIfakC=NFaqSR*!E^ECS2pp-WP~W&qTd#D2KNacFbFGwZ1bgpnG!c{h zi!;gJOlj!Dxmz>#1$Xz77R`PHUG|GHjm+b!6}zvqLf~#ce?p+Iu<$84e#E0hAYLk~ z!Z`S{@C=hyQ=IVj*a)3b<%6b9>x$0oZntu%$Hz9fU;uTqhYIs!jFs<3eDN_w&ac=~oS_sE! z@lFY58#9j@O)A%wlQ}7YbpFX~u>sVrA1#9OeqH5DAF}x5miv3EcX=^z4A096Ll@u$ zvoV_@yJV~@9~3`jM_{`Ov)GYJAIF|D{++a0!^ZoBIC&SF@ZAkiLwn2^si=s^XqAgT zmvW771q-Jlbj%OTG`5zk7XmCl)&TU#xU11I$_F(%@iZ^!jaoP&DX(;)mlr+Z4LxL7n&)!6GS6OerD?}A%y~){WynX(aB&0Ej(ZV++h^@!*GTZctmCsA0jLfPD zLN#POTBDdhIi1RnnyabnuXYsoT^V00qKRPZHT#|ALUt3uZ|+u{@(%gRT6&T%=Wx@#gezy~MHF>d0}m;>{1Nytk)>$&W^7jD zYTkn4T{OgaU!{aku_Jh{hW1s}H*ESMKW)8FgAwp0R3)-1=hM&c z{7MwKSn}&p_YfezzT-1eE8xcz*f3E(##{>+$`B~al{qHpMxSch-lGyJ`iaetYwufA zR@;TpGUD;12_&4!Q`Q-^w)o*@9H-DS;?FjI6&#+d)vFrR|50ZD3`xy6y^P#Ixu{zz z1ee0yP$x)o>cHOOM?l?1R4xh$q*z0cR8_%(z~slR*r>D63WEiX`rTh@?ft9{nYF=k zeXYFt#o$zV1g$qw!_dvp04ANL)Q?_7Em=9Mz8_khMh-K^zQEc{n_g0ypW@Bgj|8zi zbEt2Si$WW4=gjd*DiA@3#@ud;;BG<}Lw`X9{eGsVwYLtZVe3mz8yVRnlrMF2Z7C>~ zCXAh+U~5E~i((nRZkDL%V>p9N%5vOiv^Tiie>B8QC|jdCBQom<`lyQ&Yp)pAh;3x| z7hXox-y%pYJirqy+PQizo!V!Kbyt>ZTlhU``leG`S5;L~{w)W!%YGTN!*WPl=;@!Z z!m*FqUTis|Ez|FS0xsw)Lx4n7AkQhKW!*k`{n!w*fphlN3yQL~RVwrMcf9RzKH>Pc4waTysNMU)b~WF4m0ah2 zaCy^+*ztZEUybyMwH9B5_SbeVOz5C}OTWF%_b>FKW-O;D-%|ahoG?Y&H=6!Wzg0Yh zqo=Epo4GEN*P|zUEbqkk|Df(r(}8!^;c$5@ksjTXgx32YLcZ>J z@nFZ?U~eiAfCjW9tLU+n=P7nDQlaZ&=|?U?le#AR$>jmnD~0d?4EW1P{@Eajo*`PTsMr{rc%`ED z1Y-a6_g2yw2R$=&7~BdwwF+M5{_N-VFhS!w*Cuz>UHFFJ(AWo0oU@y3+W%1&i z%)P$-6Yf8RN`4vDVz=Y|yNfC1L_5QWQhzFMRUZb8aS5KFVhr}TqQaY;g{jlG-|VkC z=ei&V9bwh=!MhEIzf z@j1tRQJm?58(fItJSq*A9TKI#k7PWhG(G>}z0+Pz-J3VXDm|N(t)yzNitp;iO1qcy z^jF*K*_p|;ykjIRS&1i@EQ$mqAG`=YLtsC#et!vmh&fsyfwyLhu}4Z%$Es;bXS5OL z6HjhfL1dvRg1%;VTK94HewqICXu6hnwq|{aQR%?rW{afJ6-BG@-P=K-V3B zI}7|yc)Y}H4;$G!I+2>J+yGDSn>j&{S6V`ASo|BUx(kJG)He&LbIncf> zzO+_-f>_mF`I7_nKokL@a&{y`!a?Prt&AE-dkg8G76}e|v<^?Iv6mTat{y_6Bid26 zJCjYOoO3G-;gKgxrZyek`K^*CIgsoQIGf3-=Ti0JBE>R!Jv z*;D4diZg;wGpfBFW>x>!ciM*2*I^yER!Vnq-Vb$Xz^;ctR88R5G8a%M$G>CXa+B zEPbAZQ%mpo`Q2R;DXPb$h)6mPM8*}JP+ikMZ`bz&#IZHBC||e@_xYjqKbx?Hq*SliUf%spiJ1rEfpUjT8@Agf_9?6Yx8w#_wyp_odj%I}Z90XzA; z)AMn%_A%Z5cJYp|=557Gs|~B&EMX$EUSqXH1H0}x=Y>`$x#f$9`g;Lb)lGDNnD&&F zFy#0ZihYt9e@do#jEVR;;)D|(U&*$_=loIJf6IO%eIYE$l{5RePJy6B1XJT(>Wmbh zRB02-X7P6J&4`!(OLCBQw3ubwQN9-u(!8UQU_9pPGU`Zz^NVgw4KKoc7|jydD{Jas z+fiBjhjnw6+e$sQPv8ryBhM$3jj$$1d<1d6J$?xwd4mR_VYbA?C!!3=Ib(su4J#Wb zx|1MIzVQaV0yGF$=5oiT;wA+?vO(CsFJOVS_g0S7MahvZaLt-K%`gyD;eJ=_GIe5P4k4+}GtEnINySDVsTJ3wG{TmD zi5+jLmw3i5d8!v%=v?X1-Yt%l}{5Zsa)bSxm}?71kOE70O+rncx! zBtK8x%-KESRQc;f4aLtxmlt~GiS+TUrWgg)^v<$6aZ;LkLFQuA2%OK7I@N^zLPUFh zoln!+bhuc}`kT^~mk{ar`L8#_ew`wB?0HW!LX26Shtb%K2uJwuYH?+QL7gaS;+(q% zBpRD>Ow49vV%ql^Rbs8-7omNW%d$QmSxZvK^JrvCvL~XAw3Ah#6lxW=>MJ<^?t{{| zOe9I|kiajCW)izNc<{AGABSTji5~ouU)C8g;7I>nq=tt?WTS5uXO{4l^rtzaexqJ$ zd;KGt(S!cNcc1uN`c~@GJ6rcnN(35$5*HMa@b5q5qx9Krd8|rL3$4Nh`x#&M2CB|eVSL_63 zViL#FUa~2za9so2Fda!%@Peuel;?f1n@?nWzTnrpFFnwwTj=)1#gXWuYO_q zgF6}HQ9KH_x?`OzPb&0>9WLanWRQ0l!4c(?X|LqLhx*J;!Df1|Sh$V$Rh#C=`u9|x+h0(ruTeXv0>2Aitm@M0*#+%(=_N3jLLh&c z{lagmv$;02Y}LINB9n)oa!X3F)y_Af;~lx3%k%f%1KQ5ADy#-wDz+ER^JOvmu%M`? z)w`qoSWi9G(}AG`;XQBO_4gOQo{n#C^MI1Dz4jrmQ(&Fze1k`Q*tEQb9tMS3mr@Fj zKdwkcE(Cf@Xalfl6$eAC+6)pHej-5BAFIf|z4;#S^2LyGvYok{9(saPR%wbAb-Y6R z3a_=&B6WVT>S}HuJbD+QY#Ev1uQ~bZP$q@c_*X|N;Q^D?*#G?MEM2~7kQ@mTlLL(h zlW$D(jePm1vDkZb`ORpFNdpFBC7*GA&2j(if1e(_=H3%D`4JA1b9h62`c5&R>qgCP z`#Ts9S*qYE`)+*pq%TO-xwJO~je47p#kc7qz}CUnoKo6@ydT9fHwju>tu$QZtGlBZ zIINLSF!zEaWd?UFvBD=go9sOf353q7)y4vBt|Z~-+(mC$#*92h(SA%z zGyi4MM2$WFE-TM!#`WtNpV*RH{oFUc*i_l06eig{kIT$d{XhR7U2ho`)%%Bg58d5} zzyJb6cS#Q2-3`*xAR$P1hk$^Tpma$}ND6|KQqn0U-BRb~_g~L?)_HMWvlcLW&%XCJ zuIqDQh8v<_-3!0N8yRY4zVVGcjSMf?z!dZh2%~x10$)V`H~o{5k6e!{RZX)DoeA<&AC=+-|s@>25_jb<86$iEo- zH=4%ruVJ-$`FPXu;`SknNBQ26ccR`HUHq(g*MWaAQ0u|PKs}HMbzVF2m07xGgalVH zd-;|b@m=Q<*pNJr&N^dxEMZ#da=53V6Q8NxP1?gEUujr<_agatxrtr$IJe!hyXKm- zoW+M9-9e2LW0<}fJNu4Bea!BUqz|F_(L$85ja!uL);DSz7d>wdf#3%|QJzJAkF;|h+K9&{6K8jTjRI%~V#6cm|#x7_RI@kB*9%2Lyg zzRyH+04=8Ac5guANQN@jtP^3=DNk-BMb*Na(lYLgs2OMvZ%{n~Jw<1Nr87I8_#0a3 zOS6o|MG{+J&G2SnH065za1hb9=z52E;P-C(qafxAc0;$bG}lr+hUYy6!s2L+iHkfr zw;FlN>;C-IZ%bcX5e}XbKLq7K5BU%w7C*!x=Rxw_S%Ks)FC=QyH(R}_%CZQEX^|`y zrm~`wdmU=#26!u_nXTUPwI{6w;TnAGsR)4%YXpct+8vu9o?)FbX$Yu|X#H1NS z$j1%##U*TbFE5}53?0x|Wlw`oO%QbQM!hdHq+qbWLyL^vy5vzAv8na2rEnY@80?gkuJgmfB#A4a)i_RK zOh;PVS<0)_=TH_LX=wYH$2{0bRsuy{J6^S$D3IXNGFRx!zTWYDQykAF*d*2O@aXwG ztQTp4@KZiK7$82`I{~k~e=!8zVtB+8Ue7$%Dp)UJa%r{QNUIOP$~X%m%e8Y8>XJ<-7#hbx!P%tl)O3JGB{Tb3*) zg8YtX-YKKkSQ(fZ;le6H^-6gEDgh@e9Mjxr;5#RzwJt z%Tye*cSquB9L9`Rq*7w)N(P6(1cv&F0vMtdY_}#_WR8K6&(rrOjj{WpS}dHei(e4R zB{$#S=`@D5^K-^6J=2YBDvxA(Y9P(eGTjkg^so>t#BV1<0;PVyy(V^7caiC56&crIs<{W1qh=a+8rVh|g<>0^Y=L9 zOhcLVViOwT(#RH!Ah9*6i>*0I)84Ge4?XS?hnTGTm*|QK6r+(*yL<@D1IQ5nJOeI6V1`BU7$c9$`gJDm}Bn4#^+L#@ejh$&yO%)?C0QH z4G_^0twu$qPCR@2d^%RlsJ(q6!`iwDABt^j5QWbmE3ha$Hp7k>Z1+Z>U|l)$2^(%e z6;$;gy41n7&1~i)XOF9K@P8{pV?%|1%Os_F^Re6SXavd2ZOs2p{=9J+2>sq`Cq3aH zM90*QJrydEa;u53mUm0B+I&mHp!hWR$r_v@w&qgTnE}_&{d_espr!-5IYI@?JD#o; z>xfxN`nABDt!q#0LC7?%9iEBjQ#p6X9&5H6F|@pdz{VI%uo{(2(PG$y;Dk*dVK=_y zD{S%J;b2M&A*N+VHWCSko={5I;>#(fpl+Y9!!=!1|6xS4_XRcV-Cig80*4IOhJ(9V ztVHct((b%cNcq>V!7c+Hkl+SgSDEFzkRun29rv8ts{2942Z2ev$K6Kj=)^SN68+nE zR-8hsA`c7=hc7nr0(aPhz&BqV*j96Hy?TEV8^d=x-`Sz_9X7603cs5EydF#rW zsekID$3HnOb(Ah5!zlW!c$#)#ajk2H@+aw{2_|Bn_!&Fhz}M(x>Sigz9L4~dMNRP^ zb=uNSBurH^^w3n+!fjE)lr%G`g#b@hmt#VLu! zY5OgF-c~QfFsE0vpSOp`WpFq7{3MZ3F4&_)-$!bhvu>Zb1G~R;9nEv`Sl3k?7y*(O z|5K@JM-CZt;zIbs0>4FZlTTEG4PNvIZ3u*4UX+OS3+ew1;o3G91ZtFE)vT0?ac->s zM|evSA-MG{_O!e?$xUVN-i2UTJ+I7BeBRh>3 zo@@!`>Q7Soo)X@8V9=9`F3CViFf3zo)OUd_ErPE7eVPsKl5tNxmx(B3r2JEX;c;{t z0Xg@oO{v)|f}y`U_UCeyjG0;?|EXgms?JI@SL17K`<@vED=h-=8MIf**c~G)~iW} zI(-V$(D3}9jT)CJXsKmfyu^e7J7Dibp_RTruyasA$RXB0kM-wz@zwPNaiQBL{c`!! zo_}=G?dyv6A5x3-C|IIp@kOmhFnroo^17aA74o2B5agXlf%hLnr#IlFPtLQN06rp3&2Ojr|)lxn^s${a{(+{gRoc)OHkS;2HZV<<1% zg(TdR{4@wl^(wp8@-waC9?%ao`~`G}d7f{i|Mm_=VK)mx`sIP5ck|5Q7$5RW{b~B6 z3=Dus{ydogiBwkTH+ER$#+T}yz61X0Pnm%YAB+|~H5N$AzIRAH>H zPj9{YEHkiX@d)$m;T}9aK9nKJ&`XRx;`j2;Usg2n76G|qjdE4_ zT-WPMWc!yg;?~cB*;M4|)ay40NTmrsuuIz@?tVC zQj|R4O}a@BdMn$~NAn!~03p-O+IU~#q@5UoiJoQ8D0!=o{JHU361QwYU(hTAtD|%4 z$?K;BA&@Wo`Gx)ucs*ZC5-x${OK|ir(wk zI}3$~L7A-SFT^$P%tOc*PM;x$7zAbj2V!tagyh6Nsm$VMWW_@h$+I6NIFV+!&Hg19 zW|LU5#TZcdf%LL1@w8nMTL%*)nB2*NW6;BirY>44n_qosnHqL8-l<`DCoRw^#HsvJ z*c*5#fSF~$^skYMRVxOQ;rxAc_PtBT4HqE{{v{Cs0_KV++|yU>@9*g>B8j2M0L64= zS2vkY<-kR5!PELCr0(S%i%6%LdZEd%4&_T~m}c}A(gO!f0Pp7x;*1%r;0+!sXW)|F zGw<@t4OOiVxe0u(f4ISigWG4z$uBq8q)+Ugz9(`C&)uuuxG=M~%1_Ims9U^(jHPXs0iA?TUs`Fd zKI*9X_~%!b!%XTYSw-!M)Mfvc69SE4;XS2$j}M!?{bfz>sjW+oBJo=w>(L8I;iFRQ z*-|73!~Mq_bls3}&j7ryYHGLufrC#%5&`fc0VgqgEiMp)&$c+gYzy?T`?Udp`S7T} zFV=z?HQ0yfR_I-Oi$(mTDy_66uqDZ{PnRFU+3buw`grCS*r2JXIj?s5ikc{2-MzKa zFGl4m7G8zoPh`iyHw9XP)LA~6u|F?-W2B;fs@n$hZ}0BE_T14;=Xi&hg%fNL3jRr{ z>~T^E)B6!GDlj?B{f*F+jfKV#UNGzE)feSA6Q{&xyZtICtS4fz(C`@j1%JQVya8?K zeUlIVPiJg$!{}fCBH^4-4R|CT|C)?Xi!TJ%Z<(nTwNUlvaf6TRZz6T#3=Wpgd^DFL zF;C{`3AwZEbv+gW3sLuQ^#z_!X((AtxV*t5QWL|(Fu;clh4@HQz1yDdj2#8FM$JX> zWTQ$Kb{UZ2V=a|VSM6ocuro7v0TTkZ{WxvZrk|f5xu6TxBcw-1S9i7Ja;Z1)axn_% zQm<^l6*C#1i&2dxK485#ZivZ*LpSAKeA*pME63}IA?dN1yP=gB$^-3bhKfE$5t3EP zgN=TkK&1#lx!TG&gLJuG}EhO>>FFbY_f?@cG5j+j{x4g`+~8QY(NoxM0XSy3k|`ljT$k5 z+B!?nl^PC*yA3?`dE_n{bOyAVKZ*@e)EaA!sbS!dh@65r#>?+o)v<4scMM(jVE^CF zlS_YUZDl8Pjp}dMpG#K#WxKn8p>fc_7sFb7@bspuqVDGw4gt3dbc3Q70s#k{Wb>)= z4=VDQ=QnP=7n)G)gMFIBdQRJJuXAJwEohX`pIik0s>{;5>oOQdJmM`|Duwowjqqoh zNS${cYBba`@eR`igUvgDMG*0?iz%21#csWDwa9vjb&&cd#Il~V7~gN z2x}B;kk^%&Vn6ipjt0m07sv*T`b~m|$T6-3TUixy7SD_(R1Me4qBs z3jcqS`_2a$6CGm|B63=GrN9067Wn+Hg=5Vr(3^_zADf$ESv)T#M+^va4OAoOfk#EJ zS7FHkxrd4Km59V}eXs$h-`2!9%`L&d^{>5Fr6ER6ys7IwGStn+%}&o)I{kjMZgW1a zdV0QzqHHGLbSsdcqZ|R#foLZwvlS67DFTgJg1pCzU)3|?7ntqMZ}(0|1}fZml3tVE z6tiaUUjq15m!m>=aTNCoI#dh*0xn^Xfxf{ay>wTbPE7Yr<{tam`<47f|*ie%$*Gswuuq-$o{W-p40(_9gx>gO?il0_afGrg+%3Cr6pbXu z66#7;P5TGjThPH@sbl4EV%=0|R_U0No0B0D$_#%*p99EB|4FmM0bK*YWxP%RZ3jqea6u9BN4pz^lK= zqVflSWw{s5hBqB+)r|c_tCvuv6vf9fyD<_CrDR+ywh*4Qk|v1`wm6Lwn6XYfDb}QwBN?q*6EQa7j-RBs__*wjXy#xtmWRxp4E_w7u4z367Qg zvDryQjfXQn3jw&h16h2P3u+gR$Mc5h%WWfyq0xfL2qqT)i%j$#p1FSN>HH$ZFRg*3 zmo0^j6m6q>DJ4yfb~??-rF3Dhe8(9KGEim@XzMlbt@(+wr& zx_VRblIa~I=ok@re*>?BFxJwftnPBmxS2R|x~W`zEAr2D6-(3Fvrq8V&RMnbPMA87 zp#vV1MRzC)gjONH25_|jTMrNl(K6T!*J{TA-)S6xh#*JT|KdOlnV^2*VqLyzN;>j+gg3id8J?_6LUp;|z!I7sHrjnmERrmx&TGDFD zNlmzupE2DB-*4A2Cp;w8nvp<{OZR_Xd2L0|ac?-hzkBm_A*-;j`cz;{FOD1|^~2q- z%Mdt23VgEYNj%wo$j%8tbB!PP$tcM?x7NwO&2QHB6e7MaK8P;}GGq3NFuVVHsHW4m z?tWZ#l-i{HExMKV3{H%?4Wr8(cKDaM6B1~}!Jy8DIOl)0kGt>wq(MRy55h%tM{?4@ zfx8!0C?O{7BY7wi)(z<}!-Jy(`rnzq*bm!-57{|6dm46*CmY`%S>Kwv{SoSbnTTX# zV`I=@Pw>dL0a|qnKvV*(yF~CB&ldD2iaW%0bYGv>KTe$jh6=>$np>^NzEAaq*+B%0 z_nsKm_nC2qteD4Zk%S93HV7if%no0BxIhNV?S zI7{gTH8{Oo(YaSNTbs1r{zWF;Gg6j~j?p#Lh!7d(|EvP(lpKFOO*G^uOoD)H-JP*j z+40mD_X&Ki{yC%zR0q+&JGRoteA@rY%c{LkIVFz#0u$)|#bOq#e)y+dLl-r4E9m6a zEZe4kkCtY1GNzi&T0!(LJp0PsDuzNF20cz4&txgQQs`TNdI4lgK)qXS-O7{J(IGYq zy7SJQA9}%~P|`H21jtN)-BsszYTMwtOidGTgz(6g18krX&OO*j8rb4zJ-nx^${+3y zG$*k%S6dWD7eVUwYs3HzrNu~y(xYlzf~uUb2L=!v^-2j8!zahz%{Q;MtP+%eFNh{k zJZxm8d^-MW@~UKs4qX+Jz&eQKhBWo#g$v}>Pb7@9TTHC-N@b-7>-n0m_j3-0z(rXHF#cr5H+Ev1bN%AC94Z_=mr*Qr zR!OAtH6s4nMR0PRN&MNZwK(#XvI)&`ir`yJUQB>!s9C`-oKw7k)ZRzq?j;%M9+RuB zwY|nd`SR(zrjF&esQKY;LdiapZ>c*hU;U6E@;jad?k8KK@4kEd719s;93f|5u%Wsikv}_(7?aZbSlx@-KVj({fbr&<%5*ShUyTJyp7gaLrgt4<$EZePkVg zKvwCg|HSH`Cc~>J{6Fva%XXCsB>pAdo%-k4je3}Z2>DeK1a+D?VsQAPi`dZ~DeKpW zqFLCf-@U#$_V(o4_lXA|Evh#XCDM&4E8h=e>kh9;N0jEoLA_T<#p|y;@&-%3lNu&g zhlLHHFWw&iBoW!*BY`(Z0u^(S-sF5h0cHM6_-u4k$M{g!xG0h>EZyIr?o*G{fgv60 zk5}HSk2IZUs8!eycR&LVv>u0qOFvq`2dMX?zv( zEMI%DKfIG<#JKH}HMZ_4*nTC~nreqWb25QwI>r+N zEkw8S6J__0UD4|I90Tk0MIFEUf{!;3Fu}?Mhp-@K@Z)%v$*!c9pFbHLgvw6D`o&BZ zBaQl8JzC!ctk(WS!*e6kX)d|wonQK1tVzOm;7xler(+b4%(*}r##>Xoz9p~CRc9x( zp_MX^V)DzX1i0`~>dwmu+Bjv zjdyn->EFmECzM5>}ug(z8a4ZODl9X$TujLk!mZ z`zlFP>toO9IL%Fh({S((e$>EgFBkK1xo5J@TXhB-^SO-{!2~hTM*qV2%zJMq8hPoV z%Q5s}LaCSV-_hgZc?0q?AKo|4UORnFr~4|}BD$FX)qgB~SLRqmsRj|Z6GEyuS zN~y;?^e$V9U@hmY%M-nTj?%8DkWha|8pQaq4Q^+_?d~9c-#SR4+3cmcwr8lQwkoEm zN!acA>btryt?+cWy?^fH2f9&JDZ;eaqfu#%J~yqKJlyCK3}>j~E;@v~|32c%?UD&n z-ZYnP(JLpuJK+~7>)3FLtYJ+!SR?yl$0RQucF7Rky6U5+-1Z$ch+K=Gk&YbGG68!Z z3GyzyyG>i@L&uQhQIG%SFP6}^agJ$j&c5wp1n(-iE#>tZ^%FkNrM1q4B9spQlh?Q) zX{6Y0boEA;fb|<`cm~AjCsLpT?OSWL(EG#NI^wwC?}0swLp$zhKJgrpmg5yAhz~w# z?4VS?3W8`G_R{6fKG90JREU7dzvw z^;*;4>Xro zxt-rkYhHVTq6AC_pRWdD_kt_u6L?E-KLpl|3EKM=BSB34P@QibS$~4`vD5lxeZTR; zfv-u-SR^Xz&Lq|Dshl`Ug<at$iH% zbL^)j{ETrM`&`-HyjI!6Rqf{%gqDQ2=g0P2NchF3qe@ouwC2I{it*^w@_(lxzUm_- zgxYCn^f)OM+`TV5WR0%<@C(!9!bSVP`C?=BwiV#BKqPzKMoMc%e-AF-&qst5mzmNY z3jJ93)M`&+D11sTyj8+&D!w)?-cE3kMCpF9$&*buH;_h3-~U_$g(HoHF;sF2x2E;(5p(1oZk;-kU}`GTC%m=Gg_-VH>8IUh}DxHIn{oRS*- zC=Xhd7x8hfxv|*ewsdw2bLnO}>mF8ZRj0~F1;O|5Ru7B$IfeEHVyuY_L2uh}|GF8b zxf?|ggG}Ldi2{~_-ryVmqgf7pqd;cytNz zbY>MZu?KAo@V2jToe{a-vck!`o@o7(D|njeXD!3{=N_SJVaoCA)ZuCPt@VQ>=E2jl zZLYz+0evrO3njWA8Zb}`{2CBus&POvg3-e~kI&Zgt* zwys(5DM#28f4y`1M@KqVo_UD^GS%6qU^iCu94X|i<>Mx!a3e7JgN# z5>`_Tm>F_awl2vyR6`EsAC>Ej_ZyY*)bVuhm}TLg zC3SP*lr#twcki_4h?& z`psxX>p8m>gISI<4&&9kJ%B`WjK6>Hz*fmW{MUt}pu=u(CXes_iZJDpuum*R(94tG zHpx?VQkRsf)^>kw~Elucx;HTHwh~^8!n@9HNY>yzK z5}a_})5- z=2ZBMOLxlpgX>24I`dS)9g0sUyB>FZqR@xUL+IBX6Dg{-Fi_lzp?Y+L=?ih28k0Ne zR`y>#tFFpuX3`!NLkxpa^)d;5j&;2~nFy$8!C{xuaTn z)ag)>D#c=ZbZiQUV_o))zFyu02KxL)h{4?!GT9lg8I{&tj|^i7Wt)w=Slz%7d@arM ziV##E+3cTAa10vCh#^}rKj#3GVB-zSHv&sNBCiPpRBaO8&xVtUY6Y{a@9K2cjZz_I zJQH#qNpWn7OsDC1-nfc&*c6=tq(5%W(prEb2DdaZkGeP>aNR6&$&s+13g`N^rsR@w zW3a&t@n%xWIT?dL?gb>bgWG6vF!z&e?B2pwa>w9yHKG+^(H2NV@{aqtfR-a7is}2;yxK36KKZT|;Li zWV|hbGc{e#nI0XVQAu&J4yNj*7hK8c*1YJr|JD(O3aCS9>1 z4^O~ML~JLP2U75Q(nW-pt&~_?5_+ESn~bY}trtzVe$Iaq;^6`0WV`p*998;z&#oyG zMlYP}EF6DHdb=1Z@ZL%`JIRLK6w9D6 zBLA>whui$ddx zG&>AzFVKfE!mVh!{6hPu?~nQ7Qv{3Yis*J#(wlThrJH^DAsk+^*|VcqRUfYqVVpuK zL6m*Pt`<%OvP9uZqTPkGH9u}Fb^CQm(`P9dmMSwhoc`*{et_rJr~>ZaA&n9oj%>#) zejdnuKf318-M~+(VvwEW{ot|OnW)Hfd+x;|!YDjl;^<_zoy&SM%HLYr6nNu%E)&dZ z|In>dT^y=6wpMmHX(nv0jcC7`SO&ljI`rDV1iLoLhli+@q~{;voltts>~7XQkxI!En4~E%H*-X!EV7K;(iJ z(wsh!Xex8xQ3C7b#Dkc^-AKG7m2702TI4#RDS+61|%dla8B zk{US`PF}fT5HGqSkRQC+>b;3pOaiqZ0;HI|O?rCi=3Jk7N%~LWO1Q>c*Fa-2@a~uc zw4CIVx5U+dd8@>)o|O&*g&&w1{M`X6uo7M&b#*_SZf%M5m3Uo>Ltwgp>CaX&O80r9 z&zDnrsn|`}D zgy}n>^I0sPdAUp?E`Ia21fn~-c0+u9JOaW>1bu5JujyRC3!dNW}*j8K0V5n9?*&HpT3pH zggmkU-|7FVvoXUoYl*nAHDl+s9-Wbu0;HIg6vwXQFQo#77o1QxPOadvv@a_tNjh)y zWGzuH?@J$r)IR4}14pWuDI=n_tC10MbK4A>W!7SEj?*sd8U#~{WTVGQaR4_?gnVRE!BvziP^}5~DY~P!q z>gh990u8dvM&ARv`vnkgcm1cPrUYuKtT>LMwSkDzYae=G41Sxl6z;O6ADw%|14moJ zQ8pAuU7;OKCEjpI4L_wA+Yeb?Z~?Q|$X43nI;rU^OTRyZa$oHmUhxAJ)}{i2P}yHjrQ(Sxi={;a^$XOTCH(t7h^a3t{61(y?TgwY8nR#aFp?XAu7n zj`+m_r$Jm>`KXZa1YhL%V!v`N0m0%OF-xHv zK_(%~N(A}}2CEBn_K^5w-tp*-(PM!6(?Rys6;RsSuJ z0HjH;1IwA;9VVb{$?KC?E}yao|b%0+-eG0ftiK>!UOctI5`UWu@6UbiISFJPg{^O>eRRFb|_RO`k0 z{0&cVI=Q@(WNxd93I@SN`l;=QiWc@=e7aql_pSJrgP~K`rjvB+SP~*76WFO}RiO87 zYJDEFoxgr@F^%ve2{E_BHIGv3SWjAsa2fOGBnQ`KUNE;486-G`wUEjqKhs#{n55pz zDUaAG{yb3Q{gdGr$5tWkXjsb5?lSj*K9)w=2ySX)dSzxA9ydnD_0R^5K^ z-@|BkG}p23{f0u*b2Up@y@x`y#@7J=3qm7iIiJI5v?W!S>7n;=$K=)ek zY9p)JQ5-*wdd_?ael3_JOrfcH$q}*}@l;JtCvfmNcBtbjhm1rm)!H_|8q20JQ5hqL znr&o-5%dqj!9an^Qc!I5{AyLH4X6*hnA^8fcGF+nT;*1lEcjH9Up)LNpB-4YP`H+I zTTAAbV9|qlI5jnLP}29TaLDlz$msq=dZ{3MmXz~#>Mhut6d4Ts);8P|$A^V^3goZF z7c8K4d9}Xj2KX!m{K?}@&ygskn+jAqh)hK6@kk{DpxP8~EDmFZ@t7-FkAQO1drbNT zO^@%NDKDT^*_nQGF%+o!Ra$MPz{c)`xggZ{x7C$iG0v@RYSOKlV; zUySo`^tz*iQoORP?jRQ*w&k@wBlGJ`TZW7SG)NYMlESYq|29NNFdiqu!hj`X7sU4M z%q9YkEiCcktk1^$iLx$?MW8)4)Q(5vxEITUHP*y+8|4{AiYPI`I;n`?J2w2)@o}5z zmQC2;{1QaYDyQ*r{GI$p8<J)3);zPyLBp=(>Lv zaguY7Kpdn0h{YE2JqN_{mg}I_P!aPN==vR^d){NF7ow!9)I(oi<393zJbqavJK{hj zQx^y(@YpWjrN%QbzCGnJO!}YDtPjO{v96oY=?cGWBD7ZZ$%@`IY0i-qAQnj>inaA^ z6d3qa4>AV65$nQh2{28+9Jgt^EtSJmJ(`VL@M~MVuxzdh?V{gtcqrYEJw7oR{@FqN z1Q=3o@d_aT;N99g3|I1nmXYNz>i#LdK;QD1A_ju(XQW zUvqwDJI--5H~sJEK~iiveqX_5*b-0{IM!^R>hZoATDNGCh{gxa%Snos3G?Sh=~ z48OJhC8eacGeP@rXECl~M=YD8+q5eA{F$1S5k>bbVcH(nA%9z}kb`)336|o!E=HPD ze_9K3Zjfz7FaPr36l4hkWM{WS8xTL4w0fXC23yF4tLm!M%&Y@d2k!jPrUwY0>2-Mc z`}bvWNePsLV|4R4r#^4(wL!+pdkH>}%}|zX?7rNE-v0bqr)HrgFhfdTH>(S=XWC)Q zUR9|}FS)n;QZ8#E%okkMb=+{5leCj`5kqph+3?o;y);rQwxvb`0{T$)8p?`J7O zO!MlEyd5obSv{?I`~4W5t3p}N)0(>fdDQ;B=!saPp+aFwY_U;5FuDKGj8jILHZr`) zIVG-t=2+I1L$rQ8iPt|k<#w$lA)ZS-PWKCyD9(}IC$PomeG(3=LSD&rE!@M1kB;HD zI9qKM8Xz(;rE}M7GuOwdYDB~xuU>dm&VG%Ee~@RogF+yz zsZ!Hle9{tqI`C`9JJ;5%e&h(^JqGUp05!-$k`SuO=e9izR^QBB=o^UAiw7Y=Dmel;4h{~FIousz^45$&A%E-9j`=9I0K=bn z=7{v}txJZLbqb6Go}wSmGEFHpxjnzi-~P%e-1f5W62~u_Hp#9y_bcvs8T09)v63=u zTJ3%z=jWCvn2*8(@x;|xhSln7q}R@YKQTk{5`|6KZE!cm(eVv&SK0mX_wgmYgU?gR zm;nYf8p=aoLhhmAv!Ys-qhX}~V{eoacqh;$IwNQrumwk=Fi|-v`Yq3!*pT5qWlka< zlWsgIG5f6(`_ynsoJiCjJ;9WF$sVtdkuok&?U$eTde05V-2N6wu8txb8dONN-ib%9 z%+HB0R&e3jO46R%KHasc{9WUm$~<`Z$!l9x2ra6`hLh{(#~|fuG{g(9k02uugz(LR z@LMhI_Yxu?b@CCQ2_k$RGr*`seepqpEcct6eZi=GfH`NUvb+0t-g5#{l>zZFuL}fZ z#)J5nM-KF3UeNI{=T~1FvHs@Apq2QDOs>seh@ijkw`q>av>N#3&{AQ(j(Qi4Z*M}b z38BS2?}VgPFG5Zg_#=iWNiF**=eBGLF(Ap&6fYQ#Ly2-dmRsq- z6n=$V;f69up}?G304#EznDFse;;o#}UoD`!{!t)!oR1Zd-1r*#CFALp*`Xvk2KcV@ z<}fcsSdv5AYoOBQ5vFy{uCJXDgf`xy(#a3y&0T9fEEXN-5I3_9?PJ5#b#5r*1R2Kn z_n7=YyjfCxr$psn_YC-OI~OuF|3l+HdHsrfwXcHm71MiQFymwL#}qa+D@mC+M`_+nZ6_wZm>Hi6n>%0 zKSHl-Q~Rja?&M~8emWHB2gLQ;8U$;-)kz`t%54alH4Nh&n$gVy0vKM0jjb(1bQ^&U zn1o5`Og)qr{Kadf&?z=B%=m#~XmPIV?Lx*vBt_D_h}3z+cRB{tu1E!71p}fbmfCoi zEZ^0XwCsg~@;f+-F_OCtXlrGd)7%PUsLLA2G2kzjDE#H4VvqZO$OK$kq~bfa?_}X3 zk(!^+j8&@;nDr8=yHQ6~R(fCKWN|H01hh18KAyzLT|j|MwZl!pm<8T(_cDw!Oar%8V%AF-XF3(B-Cn^LZ0Fi zh_0#Q(N)c~oDb7%!s~gfm8Nkt5u%YpS6EWspy)h3Jqy3dN_NY?ePf2$0o=Ty9v{N2 zXrK(+j%mJOb7OY7sZ!DDQVB6DT4mpvX09c1w;FL$c(&7-&jCgyNvDj$_H#e$wF=q3 zXzET~YJqPQ%qBP7<6WYxTFgY5>cyR5aU)LFdb)~6L;e~CPN2jy_Aj16fM7l>4|NqR z7`1MUPm!!@1i$?>@RttDxqvlPhmz`&lBV1lkF)CMl;Zu z%H?u8nwXyogS}11>6@mlX^z5{)d)V(GiQRP+y)qHJ@R9VP0s^QCNzC;3)(AVk-Es` zqEj%s#2FMLV_P>?AEh~qoD4<5D$uKN-}(7W^n6kq9dBzP3%uz10E9Pu|M4JrjH!BL zl^(73T5^%&Q|vH@J*A{%A%CuFYMujfGp#3zWYvb_;^IL1lcxD1J=n zblEnya0%O=dxNmgvZI2vSqOBpfZ@$*`qxn6?*xuTQ0<@Qh<+))Ei_3wBv=PFW79LQ z6vgqf_gFXousS8)_FcFOd6yx>Fzr617`&X>A)@QhT|#XZ_u{%NjVoo6b#O-a9*ux({7uRlN( zr~Q^yh`0aO_QxR6`Hx8G1iWf`>T~0RLJabdSJ!o@k>C2?Qw+~E2=)*0w?2?0iW69C(}=d7_h zc7`e>l;8Y&DWVnDW-yv;l`2wHtc9oJ@AG#Tx0|Q4s?zJ>4X(emBZ%Gv(t;v7Y;vy0 zEax$?%iLeDnio8<{$1?HJ%$oo9j$_>O=(5Nm|lVY{{GMtGwqofTOd)*k_hTN3If4) z4GnxiC;Yq=#~%ss`MIz`f?iGnwDH)U1Uy>gMo!0LuUIfRNOQUgs|OFFwAUx9sDt z1>HsdPrG6Gp)ArR_;X~GRgPzMyVeB@*eg7c6`eA~5)qfCN@IUyo$_z2xTw6gePfPs>er2gq*8YKG?wKiC0X{?{M!`!rg6}%3l&2pv=}66Y%temEzNV-J$w=1k2I>;W!UPmIGoYwh#Es@Cd zM!5j7obuAy1(Q@!_-d!O5hTKd_VHY?bx)$H`U zw@rd?|A#G~ z7w}T>=4wExj{}Flq&WcLv5zs_AYX%H24vfBY;DmqMk;?h1d-1mmjsgb9l0z$=QnuQ zKL?oX{kP|hDZ`t|!|%<#{(r@BlLWURy4jpYDBj=lj}rVMlwg$;2h@1K`x!xanZFKw z>_BoLBwO7U@;}7s#35AuaN;Nt0n8Np3fub|ClVq)h z-qACO%OXn`;%!>C-4>iwnPblsef=gqrc-Kcg z^4Aw2|0TTN>i^L6l>t$I-_uKXNP{%eN`rLA0!pleASFmkcZUMfEl9`G4I1d;(N7D`oY}X zWPh#!q^np8qY#_T)iguAIP7_qfw1j)(@O-CreK7nHSwn=k_i5+56_h(l97!|QOTYd zZ+fH1$=hphlZ281Jl(!(>hqju__2H!%rj5%cyoq=B)jklfwB*sAFe%T_uvO1*Nh6O zujw;?&**)Iy4Wu?$14A5yu;t?LAJCbS*h(ac-HlfU~9SuSc}s>Nm*LH=^I4!I2#LP zEnmwRKo2TZUngamM6Eu};7XgrbXpdw z66C>DS-fEtpVf&XwT9Sg*gx zMV<9|qZm4cJy4%2Y8wqS6M=@}Im)8}^HT-zujop_d zNxLw*yzyBpC_-zz1lkgx)1szQCVC8-f^_rYjm)W_NOj&pXOkl6j*QoR+#}vcAqOV{r zlrx$h#IEGkeF`GWBLht~%U_hAG1Z>%oLgF-AwpudU^YE$m%N_UoMzIpV2y#dtRFZM z@a_=0xawKogg%<9pGz_tT;&mI4-B%5=+e?LIWy0F?x@h5&l*c^7G|BC!^og@QK~WP z$-oqCoT(B3^Uq6nBCFg^Mjz%y8bceNljhI8`D6PQ8Cd2(B!Mm&7==tT(adHZa#nzW zRVFM4OMCaOnBtND)`Iotz^G8q-x#Y*d!L{)geWbkT9d8|KrY&QHt zwVV91igg^^pXPLqh{LI+M#3c%)+E4PuJrjb<_8<=SRzSB*BXcSUXg$lSHLq8?&QCm zi1D65%hNRBRUeZ`(tqlF*6k6&EbZk=QHGLy&TOMNT#vjv9%=MYo%|z z69NB}9!H5&@bB-V8E z^M!`M@C)AMe|~KCi`qdJXL`7{?yHz2cCLFuNAlG_zz+>Ld&eG^y~oK8E~y;HH4IDF zUjy0$J3R1uN>+Rl*$SI654N-ConOE(fwBzSD>_H?^f0`(^Xt4KE^G$(yykDp4nI?9 z{!2A7SzBz2P9#rb(kI}nTbu>s2^$U^6>@n&pJ2jy=n@#bj4w89%P_PLjg3%_Gm z#9&Guhp$nOGqU_W;D|8{C{xF9C706KtM^yLBpUA`R|JZeJP&}f@G&p-(^Lz@U%If; zNZxHH@)-Y`KZ}pz#$F87pG`V~zRZi0wCblw&4}5V95YBy9n90F3$+_xTfIr;Ya%5x zPZRbccaJ7&54s4I&J`6`eFWk^qHL1U0O7VI2pS{0bG z572U(RnIYFY}Y!4@WfF_<)M6W(rh61%Z{<{N#D)dW>lz#Ra>WyaJAuKs#s76_;_pW3N z#a^%i+u%ZsRT^?*jY7Yh5Dd&ttE|n4Gl7t!cey0Z$&*;#=zsEHgy?iQALqHuusYhb z1!am=8|}heW;R{<`M(*c?S?0!n*Mxoz{oz*h->ovM`}QRw$E=Gr)3n-j}%-0(M-_5 zo_mji^bRS;F_A$nCm%Xa5n0~eJqQ>yOibVP{5~=qoS<*9;PLVV&M_6lccXhf_c#L0 z^02`jVhXerpQhO+>52xK3o)48bII5uiO zf1bqaBo>5!NGBvy)EwBr}`_KgxTr=*Ll^;)_D2s5Jv~+|w z`8BlMBYrZ9;u4sUoLkD7$7kb7*K5=aCtlT^98C*pruUT67oQN4o`{vAn7N$uiv(jL zcZ`=LWQE`dCi!vn6yGJP{OhJ)0#f@OO3Igc?Y1_9J0BB#`B1o?LaLDe)_etn_>j=z zGyVreXk;vanM+4a0Nch7Yp4Jghcj#f9X}I|x?8=7zpGD+i;FH`?lph4Pmp}cKOM93 zPXVD-eHW>o{_l1bEbRrKKoJWkm>Iy~oU(!*!6%PfAD^k7cmy-gvdx)yQyMpc!wkev zNd%)cz70)q-sA28Q+;q4V)Z{dzH5lS(5sD#M@_%ZksU|XGY+2byP}BN!XWCn_zVjq zkw(wZ*A8^^-2^i=bo7S@D0adB_>oISv(`R^8kHzPAb;Kb(ARO|vwxB~{(h&I7L74pUf~{?(63F@wc$p z`38}r@?KQo>dF*riNCek#VHa5S&(kNwoevb5aVCj=f~T*Me)~_a9_PX4>Rkqf-=f~p?hp@LI)7I zGZ}igBug`$cvcPcVtj7Ebzkv-_~*p44Snk zTNa=Cw~bK6{gt?`ehy^U9A)>(Nbm;}?5HGDS^y&8+fKtlt04n?j9 zM^9b7{QlH{bDuQvUu$KdyBxxLF-ulfV0{2BVs%1U(7xrU!0VI`LkH2nUd81rkC+|Y zcu-TPeB1sI7q7UPevx*umh~>!T}t$awy}F43)A_VI;ts52$al|9F{LeeY}bDk~OBd zn9!*F7CxgQ!-`R8+iy{}e|x^50)5ypzP?K|aAZQV3MwPy=Y z#7f>-G@dEK+Y2T(I3B19Z+R}}XD#+dex0N>(L#C2i?Gw_cN({1mPRAL;oQxwADg&r z-oLhe8dij3wI<1S%`rN{@b;s0N(AYbSMqGbi8|FY?1u5`WcQ zi6B`!JYNC}wZd9?6J&`);+Hqpd)^-93P#Hx;Ieu9sNxDg?eyc;xa)DM{P+$7qUIuk z6`Z;d_NyUBcSWb6qKMP#0^VFU2pw+-e|<>D;iEFQhE*=$x_7fz-zVElM?u4Qw0}I& zh}ARC+#?x=^oJ2~k?OubaWn4NC!LSn?k0DSU-y36_qu|q?xgf5Id)+Cnl#T}GALuA z$Ft&|RE*^e<$jHS)3uzpn>m}Gk6#d{3F@sF&_t56Zd7*Z_!2;}2Dd1tKZU;k>lPeW z_}-0Y(6a!Y=ANG9>(-)rWX1k(slM-r7wEOO#~6dw)F1$Gq>0vK!ODK;j|N*f-*5vw zWp^Rz8A+PO85wJ;|1)h6dF8yvK))UM&;4oB`JLk+(}TCx7o|*P(WaH!eC3sD8%=i? zWk!et^aM?!)|Oz~KE~i*70)8U+5Rtg))*_!%LLpdy^~WR6&edi5mty@v9oU$oEG!T z5I4-fX-T%87nWEakq{UCs;*Box=Q6mqU6PMAL>XGcp5SDdJbEa;DOoqFGJwRNc+vP zwDVHcWRK&>*vsA=r?puGt#|DZP5lRhMD9viDg>e@jmfjGizH_iQc0sw@BS5PBh0{v z0xP3oNTl2-_vz+kShpC}gGtboA86G9t!AYs|w{;V!>IX4V7ou2;W zuZz-~ejCTaya&fFqaOYAW~Gea@Z3|fe!~p9uR@!|(6&9FLg6!<8;8$tT#K79HQo@4_Uqrw@W_WX zo(H8ncbL}lR$!DOb&z#clt*Y3_Gfr0jU4}UPQM9o)fa{tU$A-M)omobl~R<8>3216x;L>dC}9Lqt81Yy@7Mx->ou^y zdwwXE*2OQZ6-8q0ht?W6XJkL4|7uerohjF6zwM{8d?T|$77;7<{jEd=!wCH!(rJTw zJjP)INOmI;B*t}*z~bgLHu(EfVI6d{E*Hgl1Y2dlph769;p42@d$|!Bm(Cc{7^k_W zu2&rPg?$Q&jh@aPSh9Gzm^hp?IUcy&6}jdWF3H&l;iRtl9aLLp#;- zb=xTm&X5HPegR-XV?oJ)QqO)7)MAzS?~0yVT#5em%fBOJtZv&UEHC)fJzw2QWd}+x za_96B4CI_(Grnhb>9BOaxE!|@p=G4IHGIqeZbAAe*IbH40s->pH1#vw!Ku@iGM3)u zz`EKxvyY6G3E5SlvX_bWiOZ6H{NWKsk&PgX0Q~@b(LcmBp(l2Q?B|M&4Ltk-u6qN2 zOYDRDx_@ADG;ZSmo9Nbq5tCeyGMK_=NLd+B_|g;mRo+OiU%yR5mtJqxjV|~3`}UsF z-fgesi`sZA)D#Z+NnRAsq!4eiva7;`HtwM=D2@q911E&11xqlaby7DRkrgrPh(i!! z-^k{^u6L(dx2ddAo_0_>_~YT7ARA_<@AKG^V?CHLEeeyrP6^VCn@f7rA7n}LB701E z(yUDV6NREgN>o?O;dGVldaPHtQ|b57du$~RE1Z-1vsh?bl@FQv++G@|sQZ?onD7Rr zF)yus_KGufIQ^fVZ?aKrP0_bCEun#gm>Kcx)2Iv#>*P>5>q+sV5jj%FbVyh#<$(6nRZ%{{o_@?^?9-kW)?SWIiYktw%qAA<9L6$4@}+)oPNIIL+>Ev zZxjLiVzw3B|6Ip^k`kUU=gZkJ^}n$VK0v85r!|E~Xf);g{E3oaY&y8ntEf1C6UZ&k zM$&lhfy%S|Jb-!|TtR}Y74V`!!i?2|W1ie5>r9>+YuRXq3As&jrB0Q!=i~Y+dL1Ek zSWzz?%!wq#3C0aYnUhveJK3}NT=52~WIs`A%lGdiD;4`ZKt0evr?)p-^S{OZN3XeW zEjKBis^6Z0c!cLBi1`|tvU5BUr|L^slQ&(288H|hxsBN<0LximXOIifXzDz|EUflK zun-2@dHA19&$)nh-s#Q^X`bc{Ph2MrI6J|`!bdHkYh*Ow$^q2<)S(5ZEnT@@j&vTF=Uho)|Gz}8YABFY(2RGu=;-5H@q^c_I^#mCsTL4Mo9a5%oz?WRG#E^Gx}X`}c!GJDjodX$EWbiV zrdsq-K+&0dk*r+YxgYOip*-1YRpS3_V+eYb5R1vqy6VvBl1&ZOnKO)P#CrXimtkD7 z;&U7tW#B{N&BR`50H5Z>X|^JrXyZ~G;=CtXR}mYUa6X&bKHE{Lx+8Q#elzF=29@cL znp! zD|(5HRg#if)7VwuTdXjBqWH^VdY(m1<+U)Mta2Y4xd0*r$k;fR;|2q#Y@RR`F^YWx zWdMB{p;cXTXv%A4I(^HcMx3DK;K`PaFE9$n8GQ)j_U=(%T~)u&LV$*mlj65B#JFV+ zwT{1JMU^`%4%xmlyRrp*4{gD9l;0tc5f@R%(XNZLvLzvBMQysav3m@4asI9v+Ar)ej+%P|uc8K@Wj(7F#ZnOr!;?z)%AdSuV07h6GkllAS;ilr@%+`# z)F%Plu#ET*3XTy(v{CUhu2VA~>s{Nvr7} zY~HO3*zkU9l;?}KxYj7mw&jxjnZ+NN*>|U6In9R6(h@1(bcDEtdWsBfmrLoCiNw(F zsdf=;;UE=J%M-if8YJ4y!b*RwV8RK;^)yu0Fd?WFH+m8CS0DEofR-%ymq^*$D~#usr~ntLbD1+w!b+0sOL74Nk!<2F1_`0O+lQ_*VC za!RZ@Sp6E$eU4MTuoq6VtIn>DKhm(?Fh$1pf9b5keRis$BZi>TtXG|R{6a7`S9|%_ z0MA;V-qTIxcpli_nKC>G!MG@ahH(rjc=_|R<=Hv|5)=?ZyKt&uS9$wSM?KVqujX49eT%BYH}tEs6Zj{5ky&jOur+XKAZXRNGZkN#Zo z`}D_4Ae%V4IcQt$K%?odTlD=`D=r~H(-n-<^>DZNu>7y0t#|MnFSq5$v#E~)9;ppZ zn=32foW0tHK0#0InxDMMe-+TRj6QqqPM?0+`$6u{{wY@zaUzXaL^0m}iT*G4tlb#W zgw0TGp%*Is>{0Pn<7BLpqOgQW%r;qmPQ#d+clFuDLl@GFb0^InQB+p@_0b7qCX*OP zUY?#2F8|`4&ytnHxS7ykW3?4`akkgfsp#-AC0s>{qndpSlP4lYR)&1$aL9!oY?)19 zS-+OEdegWLo0zD4!%|RX2a0a@3Ju9gcDM3TTp);Cbt5yI3ui`9&{W(|=1e|cL zDF?HpUM#5h?syreYw?-KPD*)av_h$%)`nzlV)lYddh)kd)3fJF-{#S#?ieWW^v)!mKX7 zC1^KtrZn7|SGxe z-Nopc(^#(6-YU!0WW&Bkb|a2lNS~kd1g{^RNeAi&u@)NeXw7>5bQpHxDC+F*-H3Ap zeF`}Pqba=bkE|>d1?Tn`^o6c=@GZ=tDTL#XN!NUOr@(M3v)^Xi_0)J-g`Wcd(9em_ zxAoFeJH8&p{Q-#4PSHHDy(Ca}rQr(pM+r1WCS+TB(K4Dzx}V)ESLrtG_*I_!n7)Ve9#yB5QnUlK;1nFi~}|8*lfBQ8%3MZ^|?Pvg&i^TxoANM30A?@dZK)2tHW4@k-chs&i_Q+`oWhCE{Q4W zv+T*V*nvuovk=(fxL1>@ADOu&|<#&U^7 zk7WP8egER`k>nFyDw4RrL|R{8XJKKH1lB(9tKNdZUs`lTBFuv(0;N3I z&|4sr`%0O`Pj~g$_QJ zZh~|Pfz0oYI^4LpC}}gc`otsSs(`WeiX_=_Cmmw2$5I;&*`2xPhy+N&DkRFd< z7l?T|xk$vq$&NE0aFhLpO-%=lQdBcMj$ot4cinA2RdLnHhWln(X?>g>2#ey5+uJT`Y;0VQ)Vsxnt7M3V0!8lC5inc>*~(lFE3zKbGokaw7togJ$$n&x8We5~lrF?TTdQLsokx`AUV zy9784TXPM(JiNR}6e1gVQuyr-l;9tqj-^Tj4R9pWGR-W8vB@IuUHXdde1(d&hEB?V z_OZwCt@}PRyrdO0bG=I>;f*dtf2P+5tOz@AwQrEStZ+Ucc zDgCXEM)>HsXatV<%Ls07kELf2BPc0O@Y%1hZaUN+qdY2qK{7AHswNz-O|#Cz0tZ-C z>6xEIB#Y7>B)gc5!s&hPV(N4-3}TjCmNtC#1ifSwu^mH3i9VutQ0stsO!#RDrMVJ~0V?LF#^CkwvJ@eU*n5t5w7N@o13dplmBU6a^N%}p$#ck3y$ z?t9h%Vt@J;Ea4As5vR-Up&z_FN38BLWfu-qefpxPsvOsqu8w*mpZ=}2)^^_Avi%HX zOs9LZlLyRpDqZC+N_hmpYt_-!e5E_I9%v@s^L)#i{P(_Y_6W>4J}MvH>nz5|=LX2H zKJ`BorHR8s>T{5qy;z2YSZ7LRsIN|nQ>~*E>N+q(4w^=`PsF7=+uLfdl4nwTXWkU7 z$9{uLT1FD0Fomc#vBb^0LDjWU#D9tWu~Mo{{H1?Ggu(jct~yNnMSA8u&-_brm6SB` zS)bMdRCc#7Zrx1hirp_vMssqm^)Imb{dFlTDnA9pH&sL0%L%n8T_e|19JCx?`IS3j zTNM)d&tu#=^ozyJCFewJPs+)b-Q5)R?<2j_HSceQ2j=VHJ4F4MD*eIO4-j2f+Rh7?0_#FTrdR&DWxbEaZa_5gTpu7r_S2?G@3;PB zCv&|=EU#a|^u9ZrRAV8kKbv^hRV|(Yz4H+Hmk3p>yIp?rv9{2ClY|dD2jc(cU!afs zo-T6>K_35*ymFTE`Y?hvhO-3oL`k_5xQgp#i)TnNGJ<3&NYYp;99D2A*P4njL=9Rd z$cUwgmNF71_mNdJh5t-1?c44MVH~W>taw|OeQVdjCwqPds|ww^Z_Zi((l1n^zL+2@M6yC z=qE$;&wqZmM+(lnQ%{gku@7&4JzTEh-QM3EBn#M3jEsyNf)aK>AL*d=#`aNLSvkA+ zsP6f0IcmT5^NG-E&xxpj?cA5g>J?l%^@p8UM<>G)1M^$bEnqQ;%{{Nx-}f3gqcf^T zDcl9TXa(_U3BCKR1BxYzq;o-$s4&zkb3gWr>0QJGTr78O{6Xv0gA+=u?`+gMMJsYz zprg9DtwUxk7e62V{ZlrI&U1rqdq=Zj*k>++yFqtRNbI}1%V60!|NITVT8}vWZ^oXF z{ProBJ`>8CHxRe?pCuKgi166-G_ z=a#N_V6Sm~oT_A+wB}1O?}Cdo@Ixy(Q%o^G3Aox>o z6wcWyX<$IjM}gz$=yL6X|2k;P7Z>&iG(E7pZVy0V zOY0@M1l7t>C^-3Wj;fXNX!mJ z)#?{!-^@HlM#f|zC#E6MlUL7ms*yoO*^TpZ&i@k{UhwfH^oxhev!uz63*4|#y})Wx z4*9dBVI)ZXXCnCGl_A{V9$7GmKuHGoyH78SiA`3tZ-hw0-R;2o^`!VP_&y2-V#S8- zecRMC++dLfPAR!E_a|p7iQl$j2zR@AzQ?&gf8`@)64-Tq5KXkjP;^BLcT^bjYgg=_4%!$i8Wfh6i_l{=C zmIea__IXv1rN*nv_jFR87uK;gvzZQ_8PScE+W3hYu=SD=YD3g0YWuT_<$Rv$L{jx6tG|H!H3E;YxI_ zvkn?W?Tvm|ga1f#Prse4)3P~qYyR}Ue21CSSsK6K!|hg`v)%6#@Btnt)^rZQsV_&n z!sla2#*>4k8Ymf=)D1=(vD4aKQIm@g)04(-cpzOXM@xbe#S6nNo+5Sj zHHW9EFoOaauR=Q8f0X<>F6o&wfQ2uxi9gOJb$!28IlyowW`pZr@i}h}MK&H$>`7^b zAu3VZqrcYNAMFBtV)l=@iaX7BExa&FQ#if+&dla^qP4K>xSX^JoWqWBTt#DCsLD3! z(j&!QK4&sH^lei4YAk$N&4jQ?*R=PqHA7j!Ac@x!`UU|yKk-uhoPMcC#wV!)Nu9`= zGyR*7V3!%w#`N3!7zQ<38$QB0o(o9{$SA^a7PXJA#t)W9o?)xq*2NZU&Zh7L(56`+ z?hM}VHywCtZj=-j`j2j(>bkB*zFY}FqefqN5*HKK3&j~vxWc<5 z>5b?}<~EV`c>ZiqMeECWCxrGz9qAz!rIC>elj24Os zUhSlko!Q7rD}0;AQ7cAJ5Zf)FU=_@^eiz(pExo`pXvO!;6b|}U!8B|oIg3C_%Rmz! zB3GaN@vKT&gYP6MSz$hO%NIJ=+Xb1%)77SQqC^C?8N@e@&I3V>7Zw*vDMl}?*6P_V zh_!^5KH8rFu9eh^y`AjjYCgDFtnpmY)LZfLjH~$1VSaHSTBE{t`YYAm4=4oV3urW| zr(AVLdf*)^M zV*Dl5+i*F&Ux=EJCR71$-?Q6$S!!Wtbgzqjob{?0u6VX{gcT|x6}pVuFkBD54qKww zjXej<_U&RO0E!5=myW;KANWX|u}ys%8u$YuO9`_@~Ef+26zUVSpCj5`?qV>o>sj};o;Yb@GAG1x)g z5`Wal1Ea(X%69sI#}p-NXrbCE588J=%IY`70Deb+6gFY!ztSMu-gR)8uXN1Z6ve-H zJ}9ZeggPcl04gh%-%Eq-{2`ZJU~V4c=HV}Ye(%B|wVzO_tI=7-3$&vurmvs$33U6T zjA_@OOWb!QXLKOxm0}P@Jh@D6`z+pP~jG#hXaohc~ zt6r2m#f;EK#n$mBL*7{>dZ-HTS+j;tdB0CYhk-Zx&JfbNi)S>}-LWo0`UqlE;n)7m zKIv?Xb}xbzH2``>$A;zna%Z$+3~^3JWE@}NHmHQ-D_01}&;RCT^P#3dNH0=O;s`RR zusdG_X#mnLrO$qXJ6JVWsrk)v@k{|+(JeP@tQOB-6~no~ng)`cpVQ;xL6W*;89`(j zbS~gp7;g~zL_cv0jZ*AiV*MY#Hw3;Dy`4| zc?SmJ3(=@VA%cSH01F@qc;nzC^mEq8upNIj*XU4>GS;?_01h95yQ`wzL zD50<-;NQYuj|r_6`y|BDmpyKErnA?wzC~2?ye4*Oq;xiU{rfoP(+-*aKB(yzWJQu5 zT6^Irlznt!iUDsDqqJHK{M^mXa~vWxp3mvU^(r8Y)pAY&NM+25pO4}-OLWlZzwbe9 z+8vSbwo27<7a=M!VNZ=dh!0~swlngy&pSpCB7Wg}ZCSg?=Bc4@n zSw6)WsheFO++R{et;iUupa}+DQtj#Vl=GXw=K?fNO$}1w%2yqVd7L$GM0sJ?EQ3&l zzkW`?mbpWST=uXyKudddvKewL2u-GPJlp;ag|%nBS^BO(E0+_kf!I>oi=o`IH13z7 z%G0j%-jbU^)7s=u1NZ>24xeKtU}k3oe)eNs>aN&q+Ohe_{CH(ypT>uD`Q_Oriq8@D z9bnl2sh^gE(FB>mpZ)cA`lZff1_zaF1-&MG&v^3{qZEL7k0aQ)nj)1l6@-QfwoqO> zA6Q#PXw?1|3n7vuXO%{`e4*JVemwj-GT!R-QPA6x8MOKu`;MPbAvR1Su}b2oFhq}U zto?n^9z>vWC?EUB@^g!DT7@cZGbU$bv#PxYxzUc6aY5gTm0?}S0ufR#iwQ@kgY!2l zWxYg)kLa($MUfzWf+)elP06ugiN!vWvxo|+wTqlGf02CD&UY8pP9MBF zH|YZ$lEe#aeL6@)%-Jv~oYDyy&5pv0m=ndLG177N_Ys-dDH0?vco|H~31g7^!89%O z9c$c32eUb%WIW>^q`e)pBDW-;aPS}b1CIR{eLE{(n(2@P_>APQi>!-dD1=73+<1>) z6_KF(i%v>zIzf_KDyHQ8?`7dqcsg$)^6&T2p}bzMphv38GATxc3A7dwY`rjrTYW_Q zUt#&UX#A|~;~JB#lHsqR^Vj}UMKfq$Ov(`a!Ft#eDExZjhR^sUtx)2amrc(Eei|z3 zqHK*~zRucChr0=0&JKkD4H4(5+*6O}S8noQ;HTk?3=84bxiwiIt>t8;@=uO6F5PYl z!1=Ed1gkl&0mw+-YtD)TgO(C&LplHT_7?_V`x#{LQ}(Ut9Jkb>Fz-a)&TSosxozew z#@1yUMHK>V6IL2E@lBz%;i25YHznzf7=`T-1^D87aT(Os@}$J6y^?IGxlqHNnIwmte~{Z(eg;*qb+>eu!&Pxe}-R{L3C&xS=kWH*6wp-h$aiUBf?J3yR4L{&Cac zxyq@}UmMTCKCvzm1x}UWxYF4N=-1g>P>8vlH(23O`WT~O zPh*7lFb);7IhXeeS^RU?&=Y#8)hHaL@4d4D36It%^=*N%8jUc7-gQ7G_pj06QU{r+ zJJ&?1&L{9bc$WEr()tmq%5H9^Bqt}ooNT)fsxs;8G^+tNCrpHo*#+2?f@%w@U^;Gn zkrWwWGt*Dpz-y2Haij{6M1o*m2uxm4C7f05s#PV&?kIxw2-9e&XkR;Q9N4CUCceD7 z;nza=W$)w5FKjRB5>NZz731|)(v39ju=F|U&*bbGPoNW0*Qgz~_IW;Xwaq(4fM{M*+AF#I%&ce$eJjt#$cR$6;K>eI0(lN6Iuh?YkpZ8I zh8c7j#0Lg=9sw&$b`ZP90Iv;fb* z=S))+jQ`je(3FswI0BhwVgAS_x9rZ0wz)Y=1@Rg}Ycq%bLViOa;kXupr}m_HMz#89 z{1GNh8C=0g+(9j@CWQ`+VQv{f<~t7@+Qm)GusnTyKTK!63a>5R*J!>^G<+^i;)Noo z_jOVKCV4*WI8VD>$CVNi!?X7gBlt08*fmWlJc+1oS!^7|oqb(r?W-Ri5VYmj*#%$u z4FkR!rapM|XIK|=K6(1JxOo?Ld`&U>8MAAbukp*>KDXTa?t^1*Wt=FIuQXq~j=LO> zlKB+X%v?WpPT}+q?i-(%UUFFpa1d7%2vQemi{i`|0*5uah3{*`kP76!lfpaSZ%xwK zG=1}0ut{+W8e)nkSa99Aq)i|7VFyYw6Sb0-+-(IKA0m~4H~ZJjD4L7jCph5H;1*#kCWCDpFA0|`&`&&F9*F?VU^GB0sqd(NRzT z=CXV0AV&$EF1Fi&GC`-sNTL-h>P<#Kf(QdKG`QC*LgPczxscNFdvHJhxLfQW_%4$# zJ!*nnEPmBAMT|!YLDlj^rg4~jA9YNeFu9dFSMZrTILvJ$=l-WE#J@XslHQ{~P`<8K zguJ*6vG_{wBh`iY{?J@tKOT+0nkKUO5nXgTCfAE5Bo`40q|9Vgmhq(oD|<}-ER8N- zD~XSO-Z+xp(G8!8ga}Dk%t``Umhh&q?k$Xw5Hx8xB*#M{+wy~FQ?v#@KjUsBT+R(!1`lcA( zVB8cj9Gi&Oy!#WK_--pIxhY^(GnB6##r0Y?%(}H8|S3nkYs37;Gl|Xve-SKmQeyd&)wYr?C871XZvFOA-Y% z_Xgf%-nxB1_6n->XRYXxl|jQ2QLg?VM)G2@EXsq2 zu54fg`UA$9tGE3mAn(DA$|3X6(wo0Hx?#W2B2LZhN&9#r@`+j~&8^LFg`XQ*hnue& zrc*7sc3Kf?O;fn425F|OjpC}!TiYM>)0+2l9oA|i-L7z(8Aso^Etx%X!@&6rX&Cd- zLoq#3rXaRTDq{AK&lA_?LQF(kGX!$<`W36l<8<%ln@q%@e}S-SVB*@&mW!=`YG62u`UG3YwLc)>t~-`!XX>Tf+&~D51}a;@hyHd z{?hh=#JjMaTW<%)>A5+*-MNNQ z>r`M#Lih6JOBV3^)!WnUw_F%pRZV}C-yBMh{q`+1Az|t3Hxgt7&?E5j@-E@&hSpO0 z6Dv`Zjg%l2w~7XNLW)Z>e(wIEE9_IeT~+lWNuhk(yj_2rjPUlf0cUh*NSZm9pkO@I z=#1o<{TY;>;^}-yBym6BI$@*lEsRA*1Z;#P6i>6yBRD&3hZ{3wjAowQm%1fq32byM zeSmaZKM~19&;2`$=~RpfSwZnjijqI{{)bpJbC8s4wJz#`twRi}i1s7T5jPOGg3dG7Q zV|{@H(jH?xq#!*k^}Cg{?7i3-7W}VGF!iUbsN=SqzsEi$<5qVI0#-<@AzcB}HRdLd zemy{G02t?%3A@=JIFI1F3Uh|W)z#G_zhc{V=L(|PNDU*B3_LNw!1;edAOnM_VeuPm zV3wqzHt`bv*8<26yuY6OA5U5~Q>S5(^DY>ZA`x*2b~IpMxhqAA@nj6bwC6f!YJ`u&)nW$Uj(P|L{YhpHS@FLwztL5KZc;7pI zZL(`G7IuyfJ*c#}Y{gk?V4#4`gYZ;vtv!->p#C=8oo^xqQnL;~-W_e_Mz4UA)<>+Z zx6Hpd#y_d|iQA+%tG=ELpzt032uKjfBejtKIyLwkuQ?(F6adkWQZ-->)cYBOu+pRt z7n__1dD76&M{S~yZ8*#abQi9tT*d89hnV?9qJPBRJeTbFhYk3-ccnATzZrU%)v7Q+ z`^27Cg_fm;s1o9~-ueuM?cZle`0Fp^@jozdyA69nrKsNti0K@pfZ%(YiI z7J1vdAydd9cXnN{R4N(?53S4H&D+Me>W4tC{}}}T7#3_he`o2?=65Z>u?iy-4mcvs3}hn~=x43>C}Bgz^crPeIkBJCVjQO*q+;VRSTvU08MD-+3kiAY3<5|i{70t5 zmNr$~G-Kxt7D>0m1+tBQG1`Bn`m}ztUg_PYx1cimaLd%x%-_3qbf3rgRIc*GDI?mTSta91vowz;6 zMn&RiIUO;pBL~J&@)2ZDs?Vz6?Q8zO>3rXBkdK4C(K|?;h*{9iWd~dZmIn1MsAMd^ zUTpBdRIG8_waI#|KZ)2z<4e!5o4VR*)2_cmY!UU3NZ^asfUzrOVhLdN8)P7WwzS@V zwHxwlJhH6FBTO*rV*7X^#fYJc-|cI=f6CfEm-sD?Qf3o=)SH`@A2oZk*a~t1i^K1W zt;HFL@n9A16hCBUmj!ES#{Xpoo<}%kZ}YRU+_*yHk6_y;F`@TEanCGpiJM9$PWg&~ z*gJ{l&1CBoSGilx?a6|)TM)Al!+1A56qgyv(2zkQKZ1zvGja%9(@V(FzC=y9rspUa zekmdj(8vNsKW+Qz1mzUS<%K{geZ;CW?y<49{Y{np3{P*XE z^C)A(+mWvhiizGLkJMJ=C+gpTB3op~_5W4&9pGH{@7p(p$V&DW5tUKM_9a=Bl`RxS zM%jDs$V_O+2$h{m%HA`Hk{ub@duH`sAJ6k2?{WO!<9*-X@%*0SX?%U}`!lX_UgvdQ z5l(zo+mT`WrAcX5Is!g)=vSSSRSo((AbM#AV4+E0Er{0a$xJT$8xsNZaa+`N_JJ#4 zwy~nCrWj^<2@japch>AY*n@LU6s<{O06xicSIR!AZ~J8?6sxN3H1#;gKSLooQV%S* zkEMBV;N;hm46$R2sjmhZ6rpM570YMCu~jb-)(~zeBty>r=C#B|qTh#9nIo7WS9p@1 zMJ`tMt?H=R<%&mfD}NUq+JCrdy!$nb)$@l=msC-xWpC_k1~r`L)cM_7%S$GX(x;^Y zm;5P-Qv@%p9QyM+;_ubweN0rzb0zrDfzY8M zE9b;;)|2&)_SfEv{1}#+93ENr^E^b{ZSgCkz-|a^9n#4)(jy<@)tUR|M(S=NI9j!_+G0ByohUxua@`9q>tk-0 z`9p&QJmu%Lq7G~69vqnFBclXW@77L5uucU<6UoNv{fUorBq4Qih4w;=b6Q$YoGkwk zW4PA$e=m-{X&bN~sR2iwoIsK}?&Wj<1ejb-y>N2kVb#SUA9^g!V)gXvKfk2)DQrVB zUQ{poKyCaIVN?vsNfO9+t-Ps6o<=6ArZ(lRaS^<|)*#Cl1&ui}RptdY8n*J61CVV9 zf(YWnp3&gZ$NuAghaQmU(fj)aJbDy`|FodL=`bFo`+<8?g^<5$9^|h-*xX}pD?3>d z@N(YHwtVf@?vrgtWS%MIph`npawh(-u3n|r(bkkbCiH?3P5vpS=xA|QnR;KSZXwAX z6@y>SGnNI&Wp%Et6= z#m**EZvW)#Q#ge9Yz|0Z{x3VkrV33NffqM)ayaO?vPuZI|HgU6*7$Spn6w0TWEVrO z_**rU)1JQ;*nT2WmMB4nLIhSMybk&VN~6Q%lmG&Fu5CYB_`vMdBWg;l$11~=n;N2@I)^G&?+&HGMYV0uHO0vA+*A`E#B*Ho$8e|GJr1JSMdyOEC+$h*X2a1Vug_XoevEs9^K z*}(uzwVq#%@B0G7z^Qe@O~*YHMbKkZNW4yWeqK=-pnc6evmO_r4!(hygRA#ZC^Hvn za^z>Pp0-t0>5=P!3$JACmfdU^o#)LX)2)YOHOeNg4o7)hdcY8_9LqgHL$6`bvyu16 zQZc6jdP_2qm+F`c^ZoK{gz6iHL1d~?VtFyXY7966u&K~;`%CPz>jQ8?BldL%;oooQ z`P;q~KD4vZO27mC5mz6{CgDS0Cs_05Q)?`74n2(e9xT`mwHo|h^1tw2?$L3o+W0!T zv}vAuW>|p zi_vZyO~1Jk#S&=dRT~?*nCs(rZh$!T;92;Up<5;qXXhOGs7VU3Dvoe)WonD>6}|j> z+8&1|lB@Km?fldEXda#UYl1{te=o3!JTpDU#w1a`+BaMyG36rsOE;tf1Kj=KyT5O4=CSzyfvY}Ao zAkQbJsXd`!ReUHJ`Y&GM=gFmd3UQv)na{w%VUf?zS^eaG<ne3*zhv{E<{J0O{=7EJN}BLEZotx3+u{Y+G%G{)+Z(qsrMSnk@*<3C zV=u?Lb~|BM4Un7sOi3(Vo|LBu5n3{|bI|a^(LJPeogrJzN_9$D3eL^(6at=oDIJPS z5201)6-%DnrB^c&Vb_aeGFB#oDXAPoxma^)zAcrGDq{W_Ta>LyCd3vce{5~?^J{E2 z{(QtyJ~;APfp zb)D*b^^PU>oR)_o4HnZiseR|f%XJ%#PwEh3Y(>XA8roEsaYv39AJo7!;u}dT(jF6u z-T{1R|Es8WxmT`UAzqDExX`dkkkFhG=AYAaZ0faCRXgNd+h1L~#fS6jMOiXQ#Y`q`LHJX)j7O$hT zOoz3guofxwrque6&_O-qYdk)0WE{^@0n?au^L8p}tq z5v2y@TR%%!%X1}+&ZzuYaTGD|`~Af_Jp7zi`=~o>D3eJT7*)*QrN>jdpmf-aSqfsK zXqo2@1$m9UNgVTi0pLcCM~{82$|xHN7FHyoY*JCNB`1t}r|5gd9G%-WQdT_Nr3%Jr zH(xHj)_jw{)O@nO)D7y9p?l-3She1+f>fRMvEgXMR220 z*Q{cFoA^#it{=j_`>slSPTjv|QmEV!^*z0qiCVcqeDUDA{8S#x+<0C%!IUVzAfQm^IS>PkA z*edI7#&~YQP`ZM|IvMctYl70+U zlH1OWMYjiXmbTq=Ff0IXaZ&Kv^s3mm3r1ww41Z7Q-ansG2Pn47bAPUnP09|$**B#X zA=iyTO;!b@@so#(Rm~~)O zi1I-%gOP@Y|1nO*=D#xgTjqu9kwSUKWwd}Si+qv&>pkfAckk4hGi&`XfieebzDz(# z4}%UfR?s&)I=g*G_r}|?FBGcfiS0%@_ybT`z7)be8l+a2q4ZM!hp%1wRSr2}w+~yj636?+-11H-aPO_=PWJqs=#bWfhegkR{jD(4b6Q?_5w0XCbSqbGMY%&vM^&4m;rs zrOJ9}Y{j(>6&LpNvG<+l%*sL*vsSM4rEs3vtAElcrN%(AW=~JqAK48aFU=}!QnhVs zdG8Oc8n!{D7}~mhZMHsjff1j+7~vJ={F;;^N-+qnt?UKozk8b(UpXaVEaxpqwLoj{ z?tti(C+7%qb@K!JY#WK#ys-q;kfZz`G3bfm+=oQ}26Sq1n6k}#((8wf?9#K_&x@$}&v;V>J+FOx@ zf&|M ztk3=n*o}O73kzYk0N=C$%Q@6)g0ykhTaiev|jTp57Jo?vNNBM)^oZkkQC(&wR2K`_3uXknYq(e)XQx zq@{Ow;>LGEK_ORdA}x5mk6WV;(!Wikc8Wr}A7yTKsO!#TfKALEgDN$7+jb~0YBJ<# zJ61XV2--8#Emb}*_;eUmx=-Cnz9MXO)l1dgBw2lCGDDZS@N%s^X8%3NNx#O%`Sl0j zvfV~N?6&!F!n;=LR6ehz$1aNvTzje)E>vwx&=v2U`pwL3lTW#e5BkCqpNv-K<-K_? zldECCV=>i5Wps~6LU!!3@Ow-44wm9niTIXa=oSHZ{~Hf9OaeS<2H1aZxdHRqJD5bj zyXg0~4ixqo64#5AQ!Yl3T&;iZ%hHYc2X(8goXZ{wrdJMkll&7rVxdIL5iH#PZcF~s z-7(`0>$?j$)mPE;DJOUhSyg)yZX5l6c+Q}HSOhxFxmqs?bBaLKS^o?zH8u4Jbo4aA zUWYN=`-tZ>`K19EXG_ouagp+HB*S^d{pVmiD3gp`8MZeU51Q<*y;8db&*-qb<$%;Y zq3=!z3- zn9dHI2o9A1s(0UBjeD_Y=J;qA#VC{k7^g0~C!DOdc>FAn(wRQ@VV})krLzUAf1WQ4 z4arqMcl#MWTTn)>6wMsh#OXn@zO6rcN|F%1^P@;Ljk1no_R2X(Iq!Wf2K-Ig-JM7) zN)JD`_|qh>N>`)rNNlr*rLd%|d}?i$%XL0C(2iZ_+N3|Z)*q(>@B2}CRT=N9Wv2D$Ya6x^ za@K)|apyD@BkGTA2Y)zDu7^Mn*EAg8&#B`yK<;3=mC1);8>Ng(l#A{whX4Eo`iF)K z{gy=yK$ojF*2u^vO_l|8E1agtuaT+>(sX_f0>E;EV?u-FtD>#7Yn+hSM2&S~lEZiO zinZK2Z5MOge`<%mYy>}e{je6KuBXd3pe+UZZMtPhGL@Yl?43{Cx>V3>IVZQO)R2w$ zT=rFs($GM1D$fnK#7s%&( zP760%x`|kKxWbG!ZK|QEj>UN`ZQDRt!M3#cMr9GxeJNHT z)-ISG$DKZ}#afdT(5M`DN-6d0!DrYGNmvguWcQP3?;xEI07QbXT{hmy6w+E2lp$G4 zk<1lwQqj?Zt&iOjvr&2Xdd*{}vnsyvG^dX7q{3TEMb!foB{-g1*r5=BWPsW}8NddBRq; z;h{E)Hcl@b*O_f+wGc9E3`cSv8b{&E!wX+TJj??es)#?ObmP-GEpj|Tqdhhr9dmR0 zkD@)Zpk~!6;QPgJv_Vr7`?exkk@y5?7#T0v<&_j_FTK)CQ{+%<&9&tAh@?)CIwkD1=JXgp9BN(AUBqe9#HdnDU=cNX2_#k{UNUEekjc>%* z)S?`Jd)73xEB1KbAY(eq zjW5;cODJ>TLt^3yjSVL0{Y?wmTmbeWu$GJ<6r5)!|{&`(`sR#jgTJ5F0emeEzD=ShwL^1FIK4S*n zlN3{eYYg2dRiV#fE=|K_Y_o>o(NYJo9_3i>HRlN_s>kUFWLBCzFGqDIG{| zl&L-&1{yubsq)>Q|FrCD@>3ka9RV*bZqW?3x}1PqYqQcnXX8_&GJ`Y8ryWj?&NF5K z+9mgfGz<*FTU%9~XTB4YhjFH@2eMaNyaPhr^`G}ZBYS20;X|tH_B8|_M*@#7LDBN& z{_Y>>wjhL=F+iIOR(PlS+em-3vI8}JaM}w4->Wjq>#$Y|0gZJ9e)d+2GTs6{Q-IF= z@nMRm7bEH;?)zwoN5`L6i5habV^yJ$(b+s;CCupY2Z92k3X_;7RxeTm1A~v#FP{lo ze5Nl4Z3{UAb!A~moyCEujx$rSrl3axhUuB5V(YtHqx@WJwkAB{oUPVsKm+|Fu73QG zwXDgdTkl;I4zvd`W^%MN>Qc*xK}Urdx^|7r>|tDx*Ih68w>gYdkb~CZc8j2Pi+DEB z*2m0#U!mMpx7mSsM6fZi(v*rJ{Z7Z++?B#65#__LG}4SpAIea5i=-O zTX+6ZYE>g2A?SjTM+ylxGVbl^KU!#2PUsff^R3FVH8wo4*t7Uw(f(tyOwY}QsM1mi z3}-*&7lEp3;0kpnv9oXELEM|Y;e2EPo4ys! zaPs76RL@)05WA@0?Kchos_CWUAOZV1l^*_}(Mp&){9L@5;XkH6&B*U;gqCOjLdSk&WRO?T)G=Sex@ zj86I=W5mV$jjGs${SlnJDz_HZQ@^Sty9*i3XH2SneX7F+C~SmmZmssH!)qtqu~O%) zTvGZ>s1>D|t?j58As0mWE5GFDieZ)0q#B$IL7>8P-4Ql1f6pb=5jI{|?Qp$$<7%xy z?|h3@JeI|1f8TBMdnxsHUhuK!smme#=PMO*q{$=*a%K)||M;}{;WDVGJl)#yzRgu8 zAavD*)T;EQscAqJ{a#=l9duWp&_f&Jq|F43^xqI~z<`SC@?c8b=lGd5kRc+_`gU-PUob zsWDy)pNI_ef7m2zI?Y;sejEVdFEwz3n_F9JVN}slpJnRJuW53ae{wRjH?%3a&lmTk zeVew2StBD5496W+e(xg9(fI{}r~{<@A;3{IB7nbu?@_ zxi>3DNa(&Nv>QDb+CCEg9^(3_C|;6V(pXha^?T~W#pt1%&&Q4)4>Vc|hEV-RX2Ct0 z;)SkO5rCatc3tK#t6O_7oWZuHlB@^UZxZLs)b|X3dtEDe;Z{0?DSxtDojbHODKDAH zU#EG-9{h1xWud@_8Cp0gsWi1h8*OQBxSe}V&3xv=)_i|bVs+3Y77*^Ff``+2etgqg zKZ3%~At@>4yaa)|aSCr05j8GOUmlE@Lo4*e9Q+WK6yu>Qv^rud+(*qVG=0*|Efuu{wi-@lPUM*{t4W~{41J!7Y67dEb3 zzI;(vSNB;8=pD+BKHB?^_s4SesZzSKTG+kMbImN29Kr9+QzoiUTR(rDPj~1vIjPUJ z+%%0-U!ev5V{4vR#H{Ax(kaG9JOf@5dgq9=>!;2)j@XN(Y4h>MMjZNOt=>p~hW4nc zDySC2+>`vDbg)kK97>n)W_-85R`?4&KH!vcL0uzGQV{UMS8EcBjw4znmD*>6d|L(s`_BWbHSN24k7hZ+{4fp0CdJTjtP=<08f~{gx9nJ5MorNMAqRMjY;c zBiZSn4}~&MS)L_u?_tq6sNP&F0Rj5(*jr~QvX!2Jke#K)mRRK3@2afET?zhc{bFek z8Sp8r6JK}9w5jz=#uDdS1HBspp#m1GwFcVPgsBvI+mS8D$nGIe2 zB1v(Gi2PBB$HJ${uSMoc*L~KzSKiXJ{j5QA(v$+5=RTd46#MYuiA5&t zz2RBUjk$1O!f{GV4;7m4fTuyEMst{9Bw3yIXvG1NIXv!TzWBzPK znJm5}hrjzGk=r>b<~&82;=Q7R;Z#*gLMhmya|AK8=$Z&(Kk``8jURdj?w>9!r^1%HE(@eX$LPnY7Z0BL z-0SFZY@pG*(f(BAMkL}8zv;6@vsV(&8l?dTe&B8iD0huPlb@2WuMCRHuDcDnT&*lb z&^J8i+jsAZ9L5yUOcC@hYSCqSpF!vh$)ag%Ymel;`%(*(yD<^Fe)!!b@S}9FSM;K` zk2|*SP!B0VxgO3tPbMy>{6}~{yUV=MhkOzrT9&$DlQRmSjSI5SIz|-V?0mM@YLLFc z=CB_dCg?b>{92pOcBqubpI$JEMUpn2`)V!dq2OU4#@Cj3_OY;)R{YLc0f8`R=q|G{ zWuKXyE%N^3cqK)O0ewA8_yKc;6rup&NmEai5%XB#A|zuBh7JnMtZ_Cg$pk(1p$ZcU zpn5utHQogAs3;jFYimA44_qE=%7FEApUHk@Kbv2IPuVu82k#0;FQKGlVBUQyJuEgZ zu5)T1rqP6gARInr6A9=Qd6_(^g21qC^@PagU`pX_IM&KnuQtSTTttxCSmn+_9qd^6 zy`j=y@S07%Mdm`cTdB2mbzfL^qo+-)A;TG;Rz*W z2&}D@$EtzHh$7~U7ccOKN?qyzfp&m~o0h67t8d(-Y%#}!qiz*P$|gL^49ha zmG|0g4~1RTo+yTcJ|8%Lz2zR~;ozSTUlqPLDE2u;nhA)7Wp0a#w{M^1zVdjaI2Dm# zA%YL;wr==vci_#P&J)Q_U&OD=fhI*TFEGqn*?WSDL)&1_Gh5~wBNVW@4y(aEU@@MOTh(d{G* z!lRI0i)9WLf3AiqS|F6@6~CCRm{9}PgAD32Xf`VBViW9p44sKP9);|#;6mvvzqpYr z+{H9FTw5^r$N|jp=&vxg#@#=wsE%+kTN`N$t{JlBL#%){koZuAP2{8Y8V%jC!x-0V z4@|9aeuKJqX)nE0S2o7dp2ixKGx!q9B!5A`Hcr}oVc0u};3#eN4Z$>+eYpk8_{L|) z6^*Eam9mlD+u3Y_<9MTF07eEDnKh7H_n1hyN_Y|ExNt5mB8TOw1*&!fVXt8HpovLg z&+IeldmZfa(4__2oTZ03LP$8}Eb+$y{M`gQ3HC*L^Lja`=UiRz8*7Y0M+AuOznmq~ zHBQB*rcwme_!A%JX6NKQ1oiGlm^1kRcA4y*MZ2}5)^EtLe!-OpYA~yTGyUd94L&n8 zx*L74t%>7Q67*cV1BrH33KIq^Kf$i*`1^@vD_(_3VHhkjyu$c8WTjA%u~|6V@LDqi zIM(GqGp;@p4G;>I_SzVEVwn=xbKBA~!)~||hD2RJw;F!mT}OKDx;jc&A^MW!0qT{1 zJdIgE=;30!-*;|AH(DB)gNh56rCT>46tf<%NV>Y~EIbo)IDw3?Eik|#5Cl2xj19b*PDY2M&$hE5il1U1KfYfVM&U&ue|EWir00(C z)Q|jUdhDXUyY9CvO3%CK=5FrT78_Os(y0qnZk~V5d8&7&3P|8CS|=cs1F0X zl{gtcZ6qM6@SNGk>+c`f^4MgyMhfpl9i+%_l}l%IoLzEXW$T`Bzz(c<>GzIV$Y`!oVSycXb&uV8sBVqkJ^xclyM zlhCbZN-l2h(el+Vbx^j@(AExy;_ZpFN%DmDXBSZJ_FTe69&$Idq6Y6Q@UX^6h9KZQ z)1mKfa)5o51f4U1NLFb>lPc)Z{B%U64vZ*7S>V261(ka~1#ydWPFVha%?mOaW?I7v z<4OIU(&yhps^xG6uAtY(TtLgMT4df~SaRFIl2i=n!qOpM|I>Yp%;OJiPZ8v}U_YQA zQuLEu*-$i`$25R-SiNYh(bQ!~8%Gb zNeA#oXi^&JS&2D}9)1`fU-3Z&T1#~Fj6=W%F)cz1$%7^_P-i`3TFLx(cM>Gpg9ypw zb#!Q;j~ujwKKrLgYL~h#MkAjG-J89|Hgo`P2-^LW!m5u7l>1lh&D+sSIG-wYo({CD z{6qM6t4Ve|GV8rBOtWlmZ(ld~Z>H(uLrG2@OS&?ZlcHV40vh7mUkNp^rQgr&YNwq8 z$HZVq%pg(}Tm5`KU3=R^b$@TsuPKDiY%+t3B!1!H;qIG5ZWGJ$800H&z-A#c0S5FEIBVc%b6CE@*rlaeP-Pp6 z$^pP+N86Y}WYZkWVKU}dP_^%g{32*{${QLoB13?(Y=}3OB@#6=6mLRY^bu4ejieSH z@RuHeF1LcgsF*Raa`5Mrhih_=nq2OX1{Ic1BSGUcxrK?7`0>g#yIh<~(QB>VA{M=G zZXR^GruuFZyZ!Xwfzfqm{bW$sjN~U}_tuW{@$oG|A#cFuFKl>yinJH9$WS8wlLzCq z6?@Ow1dQlkW?@yy1t$bi){C4RRhiDfOO%J<{#4%)R`l`l={g66QhDgPLe<91fmP>( zL(;UY>xQlqlp?f|Z_|uQ?p=-k#1RFx4BiCQ)Yh{X~E6 z){ztlCDIlyP%VHi+V}6@M*w@VUYDuuPNB-}j(~=U?PRZj>vi8u3H~|vf?V4st^}N% zm(1EAjaVwF+9QK|*9g5BbklVlcX7r$D7urc#bu19BN4Yxl81m?xW_n4-b0H9*Sho= zhk-}>_KMUa&pOZRh8ON%)BBj-ndE!_AYC$Xl=eJlXBNDG$(T#O^*{Z@_Fp&X$iBer zKxH<01%)%QZ{f5%j^E!E1EolqFYa5I^u4DC-D(yI=OspJ`hxX?SZDv-6t!c645q9B z1qJ22h~7T0?KP|Gp3_+$*SanX*^l5-yJF7f6Zt2c);j?;Z6u2ZP|O?m<8Q(h_*WZ~c%PQ(uY6e%T`w56i@JkS+((IR z2R{m(nS~-Or96I?y0*LY56vDKc{AP5DHR~`$FaRW8+KWsg4$odY`^xXz@zhe=_cw2 zf$^yz0B^dq;CVks^qC#s*<9RwGXD`dtv5F>>i ztWp2#=H(MX0DwVvuuCy*b6j`zDKcrlnUWD*&OU zUs+uhx>*|>%j{+Yt2h=!AyW-3haeeR*+)C%3Ap}u;$Y4o^g;bJopVkq!$c{=!~x#2 z^|tIfdU|2xEE4W#FRQ5~T1u0zZ->$fm_<8MO2O+%!+u1}n_F9mt*rFlnUFQ}%P@ z|2`c$yD!qEyX7l-%wOfx2CDhj{koGhk%1>jQ`dS_b;V?tjYn*>+JQC$HUZfT3FTIm7KY$_z%B#o>p$R{s-G9yZWAN znVXrv%!$oE+Za3j^w~6qT(<6X+Q*4RBb2Tc-e|rfYz6`dCbMeEVgV32jlh;Qf@3=Q zcOi_;_x|AEplHR$OA{l(V8`KfN@cI@v4@xWYmldh@Fg8`G@03~&8%OWa_qxo$M$mV zmqz56fw2Ku1`JO|+^g2s)?Gutc99-jy2ePN(Y+eTzEG9{p#c#J!1aF|M`r7A>(?8R z+Am7y=^{ZP;~&<0TKkUmXNY2>eReiH&YtGut3^9ebOz~ILmnJTA+v>5?QJ~k`2YbP zye|V}k7xq7Hv|`up2wE|oOVVMQFt!d4^!;Ab97`ndLBbZJ|o!74Feg!eeW|yAFk}X zK|}d=-VS&`d+@}g(UJ>yzV$vI9W_G)6cioLxt4WoH~wT&(bPPKpbaGBxgxfU+c?|z z;Vu)%C~(Ra6cjW9tKt9(hF)HiwxqL0zt=riS67z-UBI(1v4T0ozI*F1YP3@cB0VAu z5=b*h1;JtjbrcD&dtViI96y328Nio)$)%OMoXU$SU%65bQM0^}!?R0Y;Xm{je^ylN zpEY<}zqk&Ki?{TyI}k7l-oVNLGUx^WI9~i$qtH$b;)X-jIXOA_9+!7VohAApwt4bx znsmETWlQrPyYuhOrl1+nuf0n&;TNX2q^)&9Pok-`Mq>`{RAy^2?e>Whm_1p=#g1dv1lmYYNRb zF8k0qZ1wRTq@|>oq|1!^I&GcE6eEJp2^tI4bmYRl! z1N<-GFJQ&oAsZXNi-bG=O!oI1qWltN(Kz*5z@l#+3K6wLX85xSgG21D~{|=dx zg7~xOGcs_fYaTM;Ew}J7hCm95;K$xYYY`SNfVtb$y&_6bf-&JpuH$fF&g z>Tv^L8hT0BGuQ~Q@|IhF2(SN~8?Iu5_>k>`-EcHOb}h4$?a#vUjY_tQ?c^vADkVta z0rH6P+T&1?W%wd7d*;q~e{f_9_rD_MY%0Sy$d;w7$r4zZ3Q zchHYNs6W(noAUu-?QMe&M3g9$1gzpNCho2R4=%bF^Zr`ipPV zSXyUm8^p)!Gkh1EeqRxfeHfSrM{X4E8)8uP<;WjFDOQ?X&^V5Wa*~IDoyg&8#Rd4i z(9S#cP>_9N)5DLM-FOd;v0~5Fsy2L($<62-0V}@5$0iH3S1Hga*aAV>C^&~^)&6*p zuPWdQ?}DoTN(WfjOOWBg0^(s1tTli6q9`Ya_fP7v2;J~?V0{GLmu|p#)AVYD_<^>R z*WI2j%bX7%Xdx3ot9QUX0oafmagv%d%5||0JjR4YdR{n{zVmH7oD(l*d|g5L$q%)u zB2jS!AC&JblzoKV96wvLX9rgDbwPomr>6vXydmxq8x=?*si>%)4!z9Y zeEISvAsl5uESCHCb8-s;+bvdYhM_#N1*a8MZ02bYy^ItMyhalBu+6lHA^_Gb>;Q~1 zqljuR{J!*&iPmJ*`)s?k=|$b#?nxK0aOfuQ?2^e|16_2xw;I<4|6@TEJ)h%G6yv{F zC2z187LPKVv7Ur(!P|oQkBN?``Rn1J-5Rxd)MwBbJ2cRI1hU)`-~;C19k##>jJqIs z&ZMvj4lq8hd_-n8@14A8&6_oE*}uEznaPRVQ<^{;_^D4NWQV`NUEXt4OK%jWyCw1- z5a222QbmD&2mlX&4l$qY_sRL$IXTwv`<*U=X$z|RRShyBQ{5uVU^l_vtl10;lrQ0% zcudehPEH8tOA*E#;9)%KDGG=|2&wESf;EinDngW4Jq{;bcO>rm_6-jU2?=q$U2E#M z*Il^zf)}j|Sd02=>e2-9Er$WIJs8a~SkcKA$d1fFO>M~00q4Q?bi}BJ>SSbcfiJ9d z-6OiAMcywLpZoevH+~L%ISe|jE9t_nVO^9@!}a3hlj?Y#^yLq2k`i4+&N5?0sU@(ixP%+_K{2yOv^Y} zCegd{aOx1&h#WkE@MB|R{UI#AD6{|my>KKv5rRq!N@w^`7d&E_kC;NJh$*3y@CrFG$xixig1=UP_*y{xLNSY6*R|PEmh{Mr3OF=7VS?@#^~Vtt38*v z3El^*UlssZPhM3u!Zappa%b(Ex~8UPfaGHNakz?6wDGZsjt-D1C75Y`({fW(Vo-ZM zFjyKo`&^C3b-o|0lY4s0yzKTAXP&`3LXb*VGBZ2zqrmt9&}sC~Bw7qJ;gWYZ;e=I# z8KijD9tx$7E*sm}svTd5CSU`J0R52!v|>L1fv>180YiEsps0fw`X(J^@FwLet*4Q7 zM@1H!f=`4<7r?)9z!^-KOnje-@1Kd4o$p<%9x%rcj-QBbo_+wNv0!=;Ef=UDZt)$% zOFmw(g$_VX4RIzv8y&*8*#G6pe`W5=LI0V#cO8y%@c-X3v`^~-NB1|((sw^z&N(h% zq95~)a1G~~Q}AS%iwv4wAE{=U(zPK36ktAwZ&xo6tc$?{hX4{mK_Rt-)7TvNdAL8N zy-5Ge!Q^*970O!hIUs8StoIVY=H>X+xPo82sQm<4Pw7a$!WD#Y zzr66QU2eF9{Rp^_=;=SWROJ_Yv_B{elQP|=EIZdh=4bZtdco4i7 z|12{60$(9On-nNI?huv&=zhZI6xR_zrBMF(-(IcW%nQUPVJXnZi78Rf;=vE%!XfVF z!7r5k|20MsBw>IWU4+mbDc}IgNd%B(?fDL=LyV)e75wE(^5f&<>^wX~5FD)<3~PC5 zc_~3s^x;PTdn?4xIw(Rw^~_{8TiU!00y8164OcbP1NTZp4}^(7ZnBI5kB(I z<3e;>1U0ts=TaV8`<8$Tt$$A`x(&(bnX_l3DgeK>h4f&qjzabpq_b(|GKmB6_}TQD z*ROwePq74qEg|Wwap@!Y4yC2n_hnH5JQbIdwUN0I&9C>fXo4VEp4s|X(+hV2MFO-&FZp2U+4+vFE%MD3@Q&dUJYIVNg7l;-5>!Tp2m7=! z5<~$Cg}_UUIQp#Z;&@*3!3?Dn_u0SO~WWmEI*cwkJUB_?K|6IaDpe1xdNaz@xNRyyh zAQ)n}SKlsyknHK=gJqe8Xsr>1@r$6S3yOc}>u1nFwiwy{`w&x(z*>F;k_R4`kf(nE zR^I}w1)z`@is%@#W^~>M!r0ne-*Et6&o5N{^%X}uc9xi!81qMZco~XOwLr>P2A;q# zK=0}yg*B^`2Byz3C=fzPy*|}%pI2Z9<|4&DdKAoWT&nP2I~FDs3IW0x{8=M7;uzSt zwpn=JYmhWUrA`PY{z$zCmQ^B5vqgaefW?l)Tj?Azz1Dk)&bS^-mXjFs98_{q0{{7!yLZK`u+JF!2$U((a|XZaJn8-zgtE;HX%_AkJD zcrd}y)-*C|V|5?Fd)YHz!Q@~ zOV5gwlRP}{V7t!ecJm#W98e|g^O$_z1k|2y)6Osh&mwNJ@x3KJ5M?zw%Tr; zlgrZb$Wl>R6MAw52AZuKz64*KkTCJi*lAv6p2Y)l9ss6f+C&)1kTO;xAVK7f^ibxw zAxIB|&4YDkg?hruiX&9zTP)$a&H?k}?OQF@2mSQ$GFryQWGR!+m`?G03(wy;ZqK-5XsMd1$&zL>eblR@)+V+0f7V` zbB6~xm{^DGqlbq$iY`$#460d&F|f>|kjB=6s21X;0$tmCXTId)ShVC4)OwpcR_l+e zqrOp;S(6++3d5s%{7sI+qfQ-^bz&cHZqF8S3J6d@sz-GW&%b5l{=9dE=@~UJt7Vh!Of|F${@Mv3f7bp@K77I}9rsbUb?R z<1a##_V6XB$|R!X15#V4ARY*OLG}ZrbvkfEC;>-&3UkOy#TLs~p%`HC`SGt!8`y9v z>4PSL*vCnbRsokjS;hyt@-D9z2pUq3ldN$af`@6E&|{SJGc|b*5XU1p?1%# zfQN%96X@2282URv8$U~=i7lbhG;9WxQ^$ z4C`1$RG&THvk(!zAUX(*>dWDdnnT(LcAW@y+QIX)p=u(OX6@k^=;!MqB-*6vJcJX- zA5;VI0{h#+z3B(gkMrWsAsd4veq*3(qQ<;rpN+m1$DkVazj>W^Frc#N_@;y!|A7+T z?|+xzAoQYIMT3$6Oic$M1M%7t&lm*WtPB0M^s5f&djWt30p!y=x5y^p{JLcw>G{G@ zEl-3pP~9(kV0F|jj$Kh#2ijO=&$;-P@lBgJ{PPC~_@FLWUyk@nI4vzI{*NR0yF0^w z5fB6i$DxLRZUhmaz{!^TzJhxNZV>ZF?~JgLjroDe;;RM*AkPbSacd&U)E-Q;#}K%~ zIPgXg1^w|!M&fPoaj5ij2^hVzh#D>ofk1sN4RvS5J}v>&5mf~s=T-zx9m?Z?YIhXQ z%0;NgpcLF3NC_yRMeIoMd1G*m8xSE5$kkB8xe2HdJw!ggJ5pmHDL@G+YEMTJa@p5j zjSf0>G{Er{f}%MD7h~X$YU0&prn?ggPl7c^lf2O2yFn-*px``Fz;xl?WfyCtf3PNp zOw8S4@a*2p4Ri?Y11L@qw0{dd(ojtRj4~dS;1E>@zPLP)y^&t#&-igDF&={g4(i23 z0j~#?g{4vH0yS2HV@Fp+&|~G6#~wW4LG=L|N0<8-;osqx9?-U|s&xPOfs#E`NkMaL z0n|GfQ3-Dz1y{Z5;3hZ!o1Ps6`be?i{}Q$T|5|4B1F6^%iTwZd2XFiJ4TdoGevqic P;D0KY)D#NjZv_4?Ht@*2 literal 0 HcmV?d00001 diff --git a/bench/repl2-tps-sockets.png b/bench/repl2-tps-sockets.png new file mode 100644 index 0000000000000000000000000000000000000000..2a7c9c81b4db7b7143ffe42ec5c30d4c78854b40 GIT binary patch literal 37347 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 z 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 Date: Thu, 6 Aug 2026 12:41:10 +0300 Subject: [PATCH 44/52] Pace the double write buffer's kernel writeback hints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A page staged through the double write buffer asked the kernel to start writing it back the moment its data-file write returned, one sync_file_range per page from whichever process wrote it. On a primary evicting hard that is a syscall per eviction from every backend at once: half-second sampling of a 750-connection run caught 635 of 750 backends parked in IO:DataFileFlush together for a second and a half inside a checkpoint's write phase, with WAL advance down from 67 MB/s to 3 and the enclosing ten seconds 15% off the surrounding throughput. The call blocks whenever the device's request queue is congested, so under a checkpoint they all block together. Route those pages through the writeback accumulation the checkpointer and the background writer already use instead. FlushBuffer stops issuing anything: it queues a staged page into a context the caller names and returns whether it staged, so each caller schedules the page exactly once and picks where it lands. A backend's evictions of staged pages go to a new per-process context paced by dwb_writeback_after, replacing the boolean dwb_writeback; pages the buffer does not stage keep following backend_flush_after, and the checkpointer and background writer keep checkpoint_flush_after and bgwriter_flush_after. The bin-gather path loses its per-page call for the same reason: it already scheduled every page it wrote. This is pacing, not ordering. A batch can now be retired before the kernel has heard about its pages, which the fsync makes correct anyway. The exception is a server without retire workers, where a page write retires its own batch before returning: there the pending pages are issued on the spot, so any positive threshold behaves as one. 022_writeback_pacing.pl covers the parameter's bounds and reload, the threshold page by page through pg_buffercache_evict, the scope against unlogged relations, and — through an injection point handed the context in use, reporting to the server log — that nothing is left queued when a batch is made durable, on both the eviction and the bin-gather paths. Co-Authored-By: Claude Opus 5 (1M context) --- doc/src/sgml/config.sgml | 63 ++++- src/backend/storage/buffer/buf_init.c | 9 + src/backend/storage/buffer/bufmgr.c | 99 +++++-- src/backend/storage/dwb/dwb.c | 2 +- src/backend/storage/dwb/dwb_ctl.c | 2 +- src/backend/utils/misc/guc_tables.c | 23 +- src/backend/utils/misc/postgresql.conf.sample | 2 +- src/include/storage/buf_internals.h | 1 + src/include/storage/dwb.h | 20 +- src/test/modules/test_dwb/Makefile | 5 +- src/test/modules/test_dwb/meson.build | 1 + .../test_dwb/t/022_writeback_pacing.pl | 253 ++++++++++++++++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 8 + src/test/modules/test_dwb/test_dwb.c | 64 +++++ 14 files changed, 494 insertions(+), 58 deletions(-) create mode 100644 src/test/modules/test_dwb/t/022_writeback_pacing.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index a7780f7002337..975b5aef52fc5 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3669,20 +3669,55 @@ include_dir 'conf.d' - - dwb_writeback (boolean) - - dwb_writeback configuration parameter - - - - - When on, the server asks the kernel to start writing data pages - back to disk right after their double write, so that the eventual - retirement fsync finds the data already on its - way and acts as a cheap barrier rather than a full flush. Has no - effect on platforms without such a request. The default is - on. + + dwb_writeback_after (integer) + + dwb_writeback_after configuration parameter + + + + + Whenever a process has written this many data pages through the + double write buffer, it asks the kernel to start writing them back to + disk. The synchronization that eventually retires their batch (see + ) is then more likely to + find the data already on its way, and to cost closer to a barrier + than to a full flush. The pending pages are handed over in block + order, and neighbouring ones as a single request. Has no effect on + platforms without such a request. + + + This is pacing, not ordering: a page waits in its process until the + count is reached, so a batch may well be retired before the kernel + has heard about the pages in it. Raising the value trades away more + of that overlap for fewer requests. The exception is a server + running without retire workers ( + set to 0), where a page write retires its own + batch before returning: there the pending pages are handed over at + every such write, so any positive value behaves as + 1 — and so, for pages the double write buffer + stages, do and + . + + + This parameter governs the pages the double write buffer stages on + paths that have no writeback pacing of their own — above all the + eviction a backend performs to make room for a page it needs. Pages + written by the checkpointer and the background writer keep following + and + . When a backend evicts a + page the double write buffer does not stage, such as a page of an + unlogged relation, that page keeps following + . + + + If this value is specified without units, it is taken as blocks, that + is BLCKSZ bytes, typically 8kB. The valid range is + between 0, which disables the double write buffer's + own writeback, and 2MB. The default is + 256kB on all platforms. (If BLCKSZ + is not 8kB, the default and maximum values scale proportionally to + it.) This parameter can only be set in the postgresql.conf file or on the server command line. diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c index ed1dc488a42b4..39b0f7f404ced 100644 --- a/src/backend/storage/buffer/buf_init.c +++ b/src/backend/storage/buffer/buf_init.c @@ -17,11 +17,13 @@ #include "storage/aio.h" #include "storage/buf_internals.h" #include "storage/bufmgr.h" +#include "storage/dwb.h" BufferDescPadded *BufferDescriptors; char *BufferBlocks; ConditionVariableMinimallyPadded *BufferIOCVArray; WritebackContext BackendWritebackContext; +WritebackContext DwbWritebackContext; CkptSortItem *CkptBufferIds; @@ -150,6 +152,13 @@ BufferManagerShmemInit(void) /* Initialize per-backend file flush context */ WritebackContextInit(&BackendWritebackContext, &backend_flush_after); + + /* + * The double write buffer paces the writeback of the pages it staged with + * a parameter of its own, so that the hint it wants started before a + * batch retires does not turn into one syscall per page in every backend. + */ + WritebackContextInit(&DwbWritebackContext, &dwb_writeback_after); } /* diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index 709b4c9635f7c..d2bb85f6dafb5 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -63,6 +63,7 @@ #include "storage/read_stream.h" #include "storage/smgr.h" #include "storage/standby.h" +#include "utils/injection_point.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -539,8 +540,9 @@ static inline BufferDesc *BufferAlloc(SMgrRelation smgr, static bool AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress); static void CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete); static Buffer GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context); -static void FlushBuffer(BufferDesc *buf, SMgrRelation reln, - IOObject io_object, IOContext io_context); +static bool FlushBuffer(BufferDesc *buf, SMgrRelation reln, + IOObject io_object, IOContext io_context, + WritebackContext *wb_context); static void FindAndDropRelationBuffers(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber nForkBlock, @@ -2387,6 +2389,7 @@ GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context) Buffer buf; uint32 buf_state; bool from_ring; + bool staged; /* * Ensure, while the spinlock's not yet held, that there's a free refcount @@ -2480,11 +2483,17 @@ GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context) } /* OK, do the I/O */ - FlushBuffer(buf_hdr, NULL, IOOBJECT_RELATION, io_context); + staged = FlushBuffer(buf_hdr, NULL, IOOBJECT_RELATION, io_context, + &DwbWritebackContext); LWLockRelease(content_lock); - ScheduleBufferTagForWriteback(&BackendWritebackContext, io_context, - &buf_hdr->tag); + /* + * A staged page has already been scheduled, paced by the double write + * buffer's own parameter; only the rest is ours to pace. + */ + if (!staged) + ScheduleBufferTagForWriteback(&BackendWritebackContext, io_context, + &buf_hdr->tag); } @@ -4196,6 +4205,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context) int result = 0; uint32 buf_state; BufferTag tag; + bool staged; /* Make sure we can handle the pin */ ReservePrivateRefCountEntry(); @@ -4238,7 +4248,8 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context) PinBuffer_Locked(bufHdr); LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED); - FlushBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL); + staged = FlushBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL, + wb_context); LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); @@ -4248,9 +4259,11 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context) /* * SyncOneBuffer() is only called by checkpointer and bgwriter, so - * IOContext will always be IOCONTEXT_NORMAL. + * IOContext will always be IOCONTEXT_NORMAL. A staged page went into the + * same context already, before its batch could retire. */ - ScheduleBufferTagForWriteback(wb_context, IOCONTEXT_NORMAL, &tag); + if (!staged) + ScheduleBufferTagForWriteback(wb_context, IOCONTEXT_NORMAL, &tag); return result | BUF_WRITTEN; } @@ -4462,9 +4475,14 @@ FlushBufferBin(const int *buf_ids, int nbuf, bool opportunistic, pgstat_count_io_op_time(IOOBJECT_RELATION, IOCONTEXT_NORMAL, IOOP_WRITE, io_start, 1, BLCKSZ); - if (dwb_writeback) - smgrwriteback(reln, BufTagGetForkNum(&bufHdr->tag), - bufHdr->tag.blockNum, 1); + /* queue, and where the retirement is inline also issue */ + tag = bufHdr->tag; + ScheduleBufferTagForWriteback(wb_context, IOCONTEXT_NORMAL, &tag); + if (DWBRetiresInline()) + { + IssuePendingWritebacks(wb_context, IOCONTEXT_NORMAL); + INJECTION_POINT("dwb-inline-retire", wb_context); + } DWBFinishPageWrite(&refs[i]); pgBufferUsage.shared_blks_written++; @@ -4478,10 +4496,8 @@ FlushBufferBin(const int *buf_ids, int nbuf, bool opportunistic, BufTagGetRelFileLocator(&bufHdr->tag).relNumber); LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); - tag = bufHdr->tag; TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(bufHdr->buf_id); UnpinBuffer(bufHdr); - ScheduleBufferTagForWriteback(wb_context, IOCONTEXT_NORMAL, &tag); written++; } @@ -4802,10 +4818,17 @@ BufferGetTag(Buffer buffer, RelFileLocator *rlocator, ForkNumber *forknum, * * If the caller has an smgr reference for the buffer's relation, pass it * as the second parameter. If not, pass NULL. + * + * A page written through the double write buffer wants a kernel writeback + * started before its batch retires, so that the sync retiring the batch is a + * cheap barrier rather than a full flush. Such a page is queued into + * wb_context, and true is returned so the caller knows not to queue it a + * second time; a page written without the double write buffer is left to the + * caller entirely and returns false. */ -static void +static bool FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, - IOContext io_context) + IOContext io_context, WritebackContext *wb_context) { XLogRecPtr recptr; ErrorContextCallback errcallback; @@ -4814,6 +4837,7 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, char *bufToWrite; uint32 buf_state; DWBSlotRef dwbref; + bool staged; /* * Try to start an I/O operation. If StartBufferIO returns false, then @@ -4821,7 +4845,7 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, * anything. */ if (!StartBufferIO(buf, false, false)) - return; + return false; /* Setup error traceback support for ereport() */ errcallback.callback = shared_buffer_write_error_callback; @@ -4935,16 +4959,28 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, pgstat_count_io_op_time(IOOBJECT_RELATION, io_context, IOOP_WRITE, io_start, 1, BLCKSZ); - if (dwbref.batch_idx >= 0) + staged = dwbref.batch_idx >= 0; + if (staged) { /* - * Step 6b: start kernel writeback of the page now so the segment - * fsync that retires the batch becomes a cheap barrier instead of a - * full flush. Not durability — that comes from the fsync. + * Step 6b: queue the page for kernel writeback, so that the sync + * retiring its batch becomes a cheap barrier instead of a full flush. + * Not durability — that comes from the sync. + * + * Queueing is not handing over: the context holds the tag until + * dwb_writeback_after of them have accumulated, so under a retire + * pool a batch may retire before the kernel has heard about its + * pages. That is the price of not making one syscall per page, and + * the sync is correct either way. When the retirement runs inline + * there is nothing to gamble on — the fsync is a few statements + * below — so the queue is emptied here instead. */ - if (dwb_writeback) - smgrwriteback(reln, BufTagGetForkNum(&buf->tag), - buf->tag.blockNum, 1); + ScheduleBufferTagForWriteback(wb_context, io_context, &buf->tag); + if (DWBRetiresInline()) + { + IssuePendingWritebacks(wb_context, io_context); + INJECTION_POINT("dwb-inline-retire", wb_context); + } DWBFinishPageWrite(&dwbref); } @@ -4964,6 +5000,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, /* Pop the error context stack */ error_context_stack = errcallback.previous; + + return staged; } /* @@ -5559,7 +5597,8 @@ FlushRelationBuffers(Relation rel) { PinBuffer_Locked(bufHdr); LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED); - FlushBuffer(bufHdr, srel, IOOBJECT_RELATION, IOCONTEXT_NORMAL); + FlushBuffer(bufHdr, srel, IOOBJECT_RELATION, IOCONTEXT_NORMAL, + &DwbWritebackContext); LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); UnpinBuffer(bufHdr); } @@ -5656,7 +5695,8 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels) { PinBuffer_Locked(bufHdr); LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED); - FlushBuffer(bufHdr, srelent->srel, IOOBJECT_RELATION, IOCONTEXT_NORMAL); + FlushBuffer(bufHdr, srelent->srel, IOOBJECT_RELATION, + IOCONTEXT_NORMAL, &DwbWritebackContext); LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); UnpinBuffer(bufHdr); } @@ -5884,7 +5924,8 @@ FlushDatabaseBuffers(Oid dbid) { PinBuffer_Locked(bufHdr); LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED); - FlushBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL); + FlushBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL, + &DwbWritebackContext); LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); UnpinBuffer(bufHdr); } @@ -5911,7 +5952,8 @@ FlushOneBuffer(Buffer buffer) Assert(LWLockHeldByMe(BufferDescriptorGetContentLock(bufHdr))); - FlushBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL); + FlushBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL, + &DwbWritebackContext); } /* @@ -7161,7 +7203,8 @@ EvictUnpinnedBufferInternal(BufferDesc *desc, bool *buffer_flushed) if (buf_state & BM_DIRTY) { LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED); - FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL); + FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL, + &DwbWritebackContext); *buffer_flushed = true; LWLockRelease(BufferDescriptorGetContentLock(desc)); } diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 25a7f5ae51c07..4b77384c7debc 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -1184,7 +1184,7 @@ DWBFinishPageWrite(const DWBSlotRef *ref) { DWBReleaseSlot(ref); - if (dwb_retire_workers == 0 || !IsUnderPostmaster) + if (DWBRetiresInline()) (void) DWBRetireAllSync(); } diff --git a/src/backend/storage/dwb/dwb_ctl.c b/src/backend/storage/dwb/dwb_ctl.c index 5edd13ef90114..83e17e0727e5d 100644 --- a/src/backend/storage/dwb/dwb_ctl.c +++ b/src/backend/storage/dwb/dwb_ctl.c @@ -29,7 +29,7 @@ int dwb_cleaner_workers = 0; int dwb_retire_sync_method = DWB_RETIRE_SYNC_METHOD_DEFAULT; int dwb_batch_timeout_ms = 10; int dwb_retire_interval_ms = 50; -bool dwb_writeback = true; +int dwb_writeback_after = DEFAULT_DWB_WRITEBACK_AFTER; int dwb_slow_warn_ms = 5000; int dwb_slot_stuck_timeout_ms = 30000; int dwb_write_timeout_ms = 60000; diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index d8d888241ad3b..20c8f8bf4fe07 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -1222,16 +1222,6 @@ struct config_bool ConfigureNamesBool[] = true, NULL, NULL, NULL }, - { - {"dwb_writeback", PGC_SIGHUP, WAL_SETTINGS, - gettext_noop("Starts kernel writeback of data pages right after a double write buffer write."), - gettext_noop("Makes the retire fsync a cheap barrier instead of a full flush.") - }, - &dwb_writeback, - true, - NULL, NULL, NULL - }, - { {"wal_log_hints", PGC_POSTMASTER, WAL_SETTINGS, gettext_noop("Writes full pages to WAL when first modified after a checkpoint, even for a non-critical modification."), @@ -3167,6 +3157,19 @@ struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"dwb_writeback_after", PGC_SIGHUP, WAL_SETTINGS, + gettext_noop("Number of pages the double write buffer accumulates before starting kernel writeback of them."), + gettext_noop("Lets the sync that retires a batch cost closer to a barrier than to a full flush. " + "0 disables the double write buffer's own writeback; the checkpointer " + "and the background writer keep using their own parameters."), + GUC_UNIT_BLOCKS + }, + &dwb_writeback_after, + DEFAULT_DWB_WRITEBACK_AFTER, 0, WRITEBACK_MAX_PENDING_FLUSHES, + NULL, NULL, NULL + }, + { {"wal_buffers", PGC_POSTMASTER, WAL_SETTINGS, gettext_noop("Sets the number of disk-page buffers in shared memory for WAL."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index e2109389458b1..2761f74c1d157 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -264,7 +264,7 @@ # default there), fsync elsewhere #dwb_batch_timeout_ms = 10ms # force-seal an open batch after this time #dwb_retire_interval_ms = 50ms # retire worker cycle -#dwb_writeback = on # start kernel writeback after batch writes +#dwb_writeback_after = 32 # measured in pages, 0 disables #dwb_slow_warn_ms = 5s # throttle non-critical writers after this wait #dwb_slot_stuck_timeout_ms = 30s # PANIC on stuck batch coverage #dwb_write_timeout_ms = 60s # apply dwb_on_stall after this wait diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 57f3a8587784a..c52c231e88523 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -325,6 +325,7 @@ typedef struct WritebackContext extern PGDLLIMPORT BufferDescPadded *BufferDescriptors; extern PGDLLIMPORT ConditionVariableMinimallyPadded *BufferIOCVArray; extern PGDLLIMPORT WritebackContext BackendWritebackContext; +extern PGDLLIMPORT WritebackContext DwbWritebackContext; /* in localbuf.c */ extern PGDLLIMPORT BufferDesc *LocalBufferDescriptors; diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 3f7f0cd1ed080..91baa17ed0239 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -72,7 +72,7 @@ extern PGDLLIMPORT int dwb_cleaner_workers; extern PGDLLIMPORT int dwb_retire_sync_method; extern PGDLLIMPORT int dwb_batch_timeout_ms; extern PGDLLIMPORT int dwb_retire_interval_ms; -extern PGDLLIMPORT bool dwb_writeback; +extern PGDLLIMPORT int dwb_writeback_after; extern PGDLLIMPORT int dwb_slow_warn_ms; extern PGDLLIMPORT int dwb_slot_stuck_timeout_ms; extern PGDLLIMPORT int dwb_write_timeout_ms; @@ -80,6 +80,24 @@ extern PGDLLIMPORT int dwb_on_stall; #define DWBIsEnabled() (io_torn_pages_protection == DWB_PROTECT_DOUBLE_WRITES) +/* + * Does a page write retire its own batch inline? Without a retire pool + * DWBFinishPageWrite fsyncs before it returns (see dwb.c), so a writeback + * hint that is still sitting in a pending array when it is called has already + * missed its purpose. Callers hand the page to the kernel there and then. + * Requires miscadmin.h for IsUnderPostmaster. + */ +#define DWBRetiresInline() (dwb_retire_workers == 0 || !IsUnderPostmaster) + +/* + * Blocks a process accumulates before it hands the double write buffer's + * writeback hints to the kernel. The same count as the checkpointer's + * default, for the same reason: the hint is worth starting early, and worth + * starting in block order, but not worth a syscall per page. Like that one + * it is a block count, so what it is worth in bytes follows BLCKSZ. + */ +#define DEFAULT_DWB_WRITEBACK_AFTER 32 + /* * Compile-time capacity limits (GUC maxima). Statically sized arrays * (per-batch shmem arrays, the retire-side segment snapshot) rely on these, diff --git a/src/test/modules/test_dwb/Makefile b/src/test/modules/test_dwb/Makefile index 24a913ea10763..a7d2f04813490 100644 --- a/src/test/modules/test_dwb/Makefile +++ b/src/test/modules/test_dwb/Makefile @@ -11,8 +11,9 @@ TAP_TESTS = 1 EXTENSION = test_dwb DATA = test_dwb--1.0.sql -# 003_backpressure.pl uses the injection_points extension -EXTRA_INSTALL = src/test/modules/injection_points +# 003_backpressure.pl uses the injection_points extension, +# 022_writeback_pacing.pl evicts named buffers with pg_buffercache +EXTRA_INSTALL = src/test/modules/injection_points contrib/pg_buffercache export enable_injection_points REGRESS_OPTS = --temp-config $(top_srcdir)/src/test/modules/test_dwb/test_dwb.conf diff --git a/src/test/modules/test_dwb/meson.build b/src/test/modules/test_dwb/meson.build index 3daab16c9e913..1ad7ad517d375 100644 --- a/src/test/modules/test_dwb/meson.build +++ b/src/test/modules/test_dwb/meson.build @@ -58,6 +58,7 @@ tests += { 't/019_autovacuum_class.pl', 't/020_ckpt_yield.pl', 't/021_replay_warm.pl', + 't/022_writeback_pacing.pl', ], }, } diff --git a/src/test/modules/test_dwb/t/022_writeback_pacing.pl b/src/test/modules/test_dwb/t/022_writeback_pacing.pl new file mode 100644 index 0000000000000..f9bb1ffbccfe8 --- /dev/null +++ b/src/test/modules/test_dwb/t/022_writeback_pacing.pl @@ -0,0 +1,253 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# The double write buffer asks the kernel to start writing a staged page back +# before the batch retires, so that the retiring fsync is a cheap barrier +# rather than a full flush. dwb_writeback_after says how many pages a process +# accumulates before it hands them over. pg_stat_io's writebacks column +# counts the pages handed over, which is what this test reads back. +# +# The threshold is visible in the counter's arithmetic: a process hands its +# pages over in whole batches of dwb_writeback_after, so what a run adds up to +# is a multiple of whatever the parameter says, which is what tells the +# thresholds apart. At a threshold of one, and whenever the batch retires +# inline (dwb_retire_workers = 0, where waiting for the array to fill would +# mean handing the page over after its own sync), every staged write is handed +# over as it happens and the two counters meet. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('dwb_writeback'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq( +io_torn_pages_protection = double_writes +dwb_num_batches = 16 +dwb_batch_pages = 16 +dwb_retire_workers = 1 +dwb_writeback_after = 32 +autovacuum = off +shared_buffers = 1MB +bgwriter_lru_maxpages = 0 +backend_flush_after = 0 +checkpoint_timeout = 1h +# ScheduleBufferTagForWriteback is a no-op without it, so the counters this +# test reads would all be zero on the usual TAP setting. +fsync = on +)); +$node->start; + +# Client-backend writebacks of relation pages: what the parameter paces. +# Every page of a logged table is staged, so the writes of the same rows are +# what the hand-over is measured against. Both counters have to come out of +# one query: opening a session evicts pages of its own, so reading them one +# after another would compare two different moments. +my $counters = q( + SELECT COALESCE(sum(writebacks), 0) || ' ' || COALESCE(sum(writes), 0) + FROM pg_stat_io + WHERE backend_type = 'client backend' AND object = 'relation'); + +# A table well past shared_buffers, so an UPDATE pass has to evict. Returns +# the pages handed over and the pages written, in that order. +sub churn +{ + my ($table, $mark) = @_; + $node->safe_psql('postgres', + "UPDATE $table SET pad = repeat('$mark', 400)"); + # statistics reach the collector when the session ends + $node->safe_psql('postgres', 'SELECT 1'); + return split(/ /, $node->safe_psql('postgres', $counters)); +} + +$node->safe_psql( + 'postgres', q( + CREATE TABLE wb_logged (id int, pad text); + INSERT INTO wb_logged SELECT g, repeat('a', 400) FROM generate_series(1, 20000) g; +)); + +# --- bounds ----------------------------------------------------------------- + +is( $node->safe_psql( + 'postgres', q( + SELECT unit = (current_setting('block_size')::int / 1024) || 'kB' + FROM pg_settings WHERE name = 'dwb_writeback_after')), + 't', + 'dwb_writeback_after is measured in blocks'); + +my ($rc, $stdout, $stderr) = + $node->psql('postgres', 'SET dwb_writeback_after = 1'); +like( + $stderr, + qr/cannot be changed now/, + 'dwb_writeback_after cannot be set from a session'); + +# pg_settings.setting is the raw block count; current_setting() would render +# it with a unit, which is not what the bounds are expressed in. +sub setting +{ + return $node->safe_psql('postgres', + "SELECT setting FROM pg_settings WHERE name = 'dwb_writeback_after'"); +} + +sub set_to +{ + my ($value) = @_; + + $node->adjust_conf('postgresql.conf', 'dwb_writeback_after', $value); + $node->reload; + $node->poll_query_until('postgres', + "SELECT setting = '$value' FROM pg_settings" + . " WHERE name = 'dwb_writeback_after'") + or die "dwb_writeback_after did not reach $value"; +} + +for my $ok (0, 1, 256) +{ + set_to($ok); + is(setting(), "$ok", "dwb_writeback_after accepts $ok"); +} + +my $offset = -s $node->logfile; +$node->adjust_conf('postgresql.conf', 'dwb_writeback_after', 257); +$node->reload; +$node->wait_for_log( + qr/is outside the valid range for parameter "dwb_writeback_after"/, + $offset); +is(setting(), '256', + 'a value past the maximum is refused and the old one kept'); + +# --- the parameter's effect across a reload --------------------------------- + +set_to(0); +$node->safe_psql('postgres', "SELECT pg_stat_reset_shared('io')"); +my ($handed, $written) = churn('wb_logged', 'b'); +is($handed, '0', 'no page is handed over while the parameter is zero'); +cmp_ok($written, '>', 1000, 'though the pass did evict'); + +# Each threshold leaves its own signature in the total, so an implementation +# that ignored the parameter and used a fixed size would fail all but one. +my $mark = 'c'; +for my $n (32, 256, 1) +{ + set_to($n); + $node->safe_psql('postgres', "SELECT pg_stat_reset_shared('io')"); + ($handed, $written) = churn('wb_logged', $mark++); + cmp_ok($handed, '>', 0, "pages are handed over at a threshold of $n"); + is($handed % $n, 0, "and in whole batches of $n"); + if ($n == 1) + { + is($handed, $written, + 'at a threshold of one, every staged write as it happens'); + } + else + { + cmp_ok($handed, '<=', $written, + "and never more than what was written"); + } +} + +# --- scope: only what the double write buffer stages ------------------------- + +# Built here and dropped again, so that no page of it is left in the pool to +# be evicted by a later pass and counted as a write nobody staged. +$node->safe_psql( + 'postgres', q( + CREATE UNLOGGED TABLE wb_unlogged (id int, pad text); + INSERT INTO wb_unlogged SELECT g, repeat('a', 400) FROM generate_series(1, 20000) g; +)); +set_to(32); +$node->safe_psql('postgres', "SELECT pg_stat_reset_shared('io')"); +my ($unlogged, $writes) = churn('wb_unlogged', 'd'); +cmp_ok($writes, '>', 1000, 'the unlogged pass did evict'); +cmp_ok($unlogged, '<', $writes / 10, + 'pages the double write buffer does not stage are left to backend_flush_after' +); +$node->safe_psql('postgres', 'DROP TABLE wb_unlogged'); + +# --- synchronous retirement -------------------------------------------------- + +$node->adjust_conf('postgresql.conf', 'dwb_retire_workers', '0'); +$node->restart; +$node->safe_psql('postgres', "SELECT pg_stat_reset_shared('io')"); +($handed, $written) = churn('wb_logged', 'e'); +cmp_ok($handed, '>', 0, + 'pages are handed over when the batch retires inline'); +is($handed, $written, + 'every one of them, at a threshold that would otherwise hold them back'); + +# The counters above cannot tell "issued before the sync" from "issued after +# it": both leave the same totals. This can. The watch runs inside whichever +# process is writing, is handed the very context that process queued into, and +# reports through the server log — so it covers the bin-gather path in the +# checkpointer as well as the eviction path in a backend, neither of which can +# be read out of the test session's memory. +SKIP: +{ + skip 'injection points not supported by this build', 3 + unless $ENV{enable_injection_points} + && $ENV{enable_injection_points} eq 'yes'; + + $node->safe_psql('postgres', 'CREATE EXTENSION test_dwb'); + my $offset = -s $node->logfile; + $node->safe_psql('postgres', 'SELECT test_dwb_watch_inline_retire()'); + + # a backend evicting, then the checkpointer flushing in bins + churn('wb_logged', 'g'); + $node->safe_psql('postgres', 'CHECKPOINT'); + + $node->wait_for_log(qr/dwb-inline-retire watch armed in client backend/, + $offset); + ok(1, 'the watch ran on the eviction path'); + $node->wait_for_log(qr/dwb-inline-retire watch armed in checkpointer/, + $offset); + ok(1, 'and on the bin-gather path'); + ok( !$node->log_contains(qr/writebacks still queued/, $offset), + 'nothing was still queued when a batch was made durable'); + + $node->safe_psql('postgres', 'SELECT test_dwb_unwatch_inline_retire()'); +} + +# --- the threshold, one page at a time --------------------------------------- + +# Everything above reads totals, which a fixed internal size could match by +# luck. This evicts named buffers one by one in a session of its own: one +# short of the threshold nothing has been handed over, and the page that +# reaches it hands over the whole array. +$node->adjust_conf('postgresql.conf', 'dwb_retire_workers', '1'); +$node->adjust_conf('postgresql.conf', 'shared_buffers', '16MB'); +$node->restart; +set_to(8); + +$node->safe_psql( + 'postgres', q( + CREATE EXTENSION pg_buffercache; + CREATE TABLE wb_exact (id int, pad text); + INSERT INTO wb_exact SELECT g, repeat('a', 400) FROM generate_series(1, 400) g; + CHECKPOINT;)); + +my $evict = q{ + SELECT count(*) FROM ( + SELECT pg_buffercache_evict(bufferid) FROM pg_buffercache + WHERE relfilenode = pg_relation_filenode('wb_exact') AND isdirty + LIMIT }; + +my $bg = $node->background_psql('postgres'); +$bg->query("UPDATE wb_exact SET pad = repeat('h', 400)"); +$node->safe_psql('postgres', "SELECT pg_stat_reset_shared('io')"); + +$bg->query($evict . '7) x'); +$node->poll_query_until('postgres', "SELECT ($counters) = '0 7'") + or die 'the first seven evictions did not settle'; +ok(1, 'one page short of the threshold, nothing has been handed over'); + +$bg->query($evict . '1) x'); +$node->poll_query_until('postgres', "SELECT ($counters) = '8 8'") + or die 'the eighth eviction did not settle'; +ok(1, 'the page that reaches it hands over the whole array'); +$bg->quit; + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index e54a14be47cd0..686f8a9834bb4 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -144,3 +144,11 @@ CREATE FUNCTION test_dwb_unpin_block() CREATE FUNCTION test_dwb_enqueue_block(rel regclass, blkno int) RETURNS bool STRICT AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_watch_inline_retire() + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_unwatch_inline_retire() + RETURNS void STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index 53f02abb08c2e..ce9e3f7c19325 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -38,6 +38,7 @@ #include "storage/smgr.h" #include "storage/sync.h" #include "utils/builtins.h" +#include "utils/injection_point.h" #include "utils/pg_lsn.h" #include "utils/rel.h" #include "utils/resowner.h" @@ -46,6 +47,69 @@ PG_MODULE_MAGIC; +/* + * Ordering watch for the inline-retirement path. + * + * Without a retire pool a page write makes its own batch durable before it + * returns, so the kernel writeback the double write buffer wants started + * ahead of that sync has to leave the pending array first. The callback + * fires in whichever process is doing the writing — a client backend for + * FlushBuffer, the checkpointer or the background writer for the bin path — + * and is handed the very context that process queued into, so it reports + * through the server log rather than through memory the test session could + * read: one line the first time it runs in a process, and a warning every + * time something was still queued, which is the ordering mistake a later + * edit could reintroduce. + */ +#ifdef USE_INJECTION_POINTS +PGDLLEXPORT void test_dwb_inline_retire_cb(const char *name, + const void *private_data, + void *arg); + +void +test_dwb_inline_retire_cb(const char *name, const void *private_data, void *arg) +{ + static bool announced = false; + WritebackContext *wb_context = (WritebackContext *) arg; + + if (!announced) + { + announced = true; + elog(LOG, "dwb-inline-retire watch armed in %s", + GetBackendTypeDesc(MyBackendType)); + } + + if (wb_context != NULL && wb_context->nr_pending > 0) + elog(WARNING, "dwb-inline-retire: %d writebacks still queued", + wb_context->nr_pending); +} +#endif + +PG_FUNCTION_INFO_V1(test_dwb_watch_inline_retire); +Datum +test_dwb_watch_inline_retire(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("dwb-inline-retire", "test_dwb", + "test_dwb_inline_retire_cb", NULL, 0); + PG_RETURN_VOID(); +#else + elog(ERROR, "injection points are not supported by this build"); +#endif +} + +PG_FUNCTION_INFO_V1(test_dwb_unwatch_inline_retire); +Datum +test_dwb_unwatch_inline_retire(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + (void) InjectionPointDetach("dwb-inline-retire"); + PG_RETURN_VOID(); +#else + elog(ERROR, "injection points are not supported by this build"); +#endif +} + static void check_dwb_enabled(void) { From 8e00a12d2ddd39c120c652267f3e945bd095dd09 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 7 Aug 2026 00:57:46 +0300 Subject: [PATCH 45/52] Stop waking a warm worker for every block replay publishes Replay signalled the pool once per published block, which is a kill(2) each time, and it decided page residency itself before publishing, which is a buffer-table lookup each time. Together those were 28 % of the process that applies the WAL, as much as applying it. The publisher now signals only when nobody is searching the ring, and a worker that leaves the search hands the ring on in its place; scanners, pending and sleepers are what the two sides read to agree on that. A worker that dies hands it on from its exit path, whether it was searching or asleep. Residency moves into the worker, so every block reference is published rather than only the misses, and the worker counts the hit or the read for pg_stat_recovery_prefetch. Publishing every reference put five times more traffic through the size check each request makes, which turned a latent defect in that check into a fatal one. It kept its own sixteen-entry cache indexed by the low bits of the relation number, and relation numbers are handed out sequentially, so two hot relations sixty-four apart share a row for good. Its miss path asked smgrexists(), which closes the whole fork outside the startup process, and the smgrnblocks() behind it then reopened the entire segment chain: one worker made 572k file opens against a single page read in twelve seconds. The size now comes from smgr_cached_nblocks on the SMgrRelation the worker already opens for the read. That is keyed by the whole relation identity, has no fixed size and no eviction policy to get wrong, and is cleared by the smgrreleaseall() this pool already runs when replay tells it that files have gone. smgrexists() stays for the one cold path that has to tell a vanished relation from a short one. --- doc/src/sgml/config.sgml | 9 + doc/src/sgml/monitoring.sgml | 9 + src/backend/access/transam/xlogprefetcher.c | 51 +- src/backend/access/transam/xlogwarm.c | 454 +++++++++++++++--- src/include/access/xlogprefetcher.h | 11 + src/include/access/xlogwarm.h | 3 +- .../modules/test_dwb/t/021_replay_warm.pl | 308 ++++++++++++ src/test/modules/test_dwb/test_dwb--1.0.sql | 10 +- src/test/modules/test_dwb/test_dwb.c | 76 ++- 9 files changed, 837 insertions(+), 94 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 975b5aef52fc5..a9430b17daccc 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -4445,6 +4445,15 @@ include_dir 'conf.d' bounds how far ahead of replay the server looks for blocks to warm. The default is 256. This parameter can only be set at server start. + + Every block a record refers to takes a request, whether or not the + block turns out to be in a buffer already: which of the two it is is + the pool's answer to give, and asking the question in the startup + process is the cost this arrangement avoids. Since most references + are to blocks already in buffers, the reads actually outstanding + ahead of replay are a fraction of this setting, and a pool of many + workers wants room for all of them. + diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index d86e7aa51e8d1..eefac73f38f22 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -1854,6 +1854,15 @@ description | Waiting for a newly initialized WAL file to reach durable storage with the pg_stat_reset_shared function. + + When is set, whether a block was + already in the buffer pool is decided by the pool's workers rather than by + recovery itself, and prefetch and + hit are counted there. A request that no worker + ever serves — one withdrawn because recovery moved its read position, or + one whose worker exited while holding it — is counted in neither. + + <structname>pg_stat_recovery_prefetch</structname> View diff --git a/src/backend/access/transam/xlogprefetcher.c b/src/backend/access/transam/xlogprefetcher.c index 31e16eeec053a..1d386191d7009 100644 --- a/src/backend/access/transam/xlogprefetcher.c +++ b/src/backend/access/transam/xlogprefetcher.c @@ -365,6 +365,35 @@ XLogPrefetchIncrement(pg_atomic_uint64 *counter) pg_atomic_write_u64(counter, pg_atomic_read_u64(counter) + 1); } +/* + * Increment a counter that more than one process writes. + * + * The plain increment above belongs to the startup process alone and is a + * read followed by a write, which several writers would lose counts to. The + * replay warm pool has as many writers as it has workers, so it uses this. + */ +static inline void +XLogPrefetchIncrementShared(pg_atomic_uint64 *counter) +{ + pg_atomic_fetch_add_u64(counter, 1); +} + +/* + * Record what the warm pool found when it served a request: a block already + * in a buffer, or one it had to read. + */ +void +XLogPrefetchCountHit(void) +{ + XLogPrefetchIncrementShared(&SharedStats->hit); +} + +void +XLogPrefetchCountPrefetch(void) +{ + XLogPrefetchIncrementShared(&SharedStats->prefetch); +} + /* * Create a prefetcher that is ready to begin prefetching blocks referenced by * WAL records. @@ -785,20 +814,19 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn) */ if (XLogWarmPoolActive()) { - Buffer resident; uint64 request_id; int slot_no; - resident = LookupSharedBuffer(reln, block->forknum, - block->blkno); - if (BufferIsValid(resident)) - { - /* Cache hit, nothing to do. */ - XLogPrefetchIncrement(&SharedStats->hit); - block->prefetch_buffer = resident; - return LRQ_NEXT_NO_IO; - } - + /* + * Whether the block is already in a buffer is a question with + * a hash lookup under a partition lock behind it, and three + * references in four answer yes. Asking it here spends that + * lookup in the one process replay cannot do without, so the + * question goes to the pool along with the block: a worker + * asks it, hands back whichever buffer the answer names, and + * counts it (XLogWarmDoOne()). Replay ends up with the same + * hint, produced beside it instead of by it. + */ slot_no = XLogWarmPublish(block->rlocator, block->forknum, block->blkno, &request_id); if (slot_no == XLOGWARM_NO_SLOT) @@ -809,7 +837,6 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn) block->warm_slot = slot_no; block->warm_request = request_id; - XLogPrefetchIncrement(&SharedStats->prefetch); block->prefetch_buffer = InvalidBuffer; return LRQ_NEXT_IO; } diff --git a/src/backend/access/transam/xlogwarm.c b/src/backend/access/transam/xlogwarm.c index 817cb9958e6bc..de209535a9181 100644 --- a/src/backend/access/transam/xlogwarm.c +++ b/src/backend/access/transam/xlogwarm.c @@ -57,6 +57,7 @@ */ #include "postgres.h" +#include "access/xlogprefetcher.h" #include "access/xlogwarm.h" #include "miscadmin.h" #include "pgstat.h" @@ -128,6 +129,35 @@ typedef struct XLogWarmCtl int capacity; pg_atomic_uint32 hand; /* where consumers start scanning */ + /* + * What keeps the publisher from spending a system call per block. + * + * Replay publishes on the order of a hundred thousand blocks a second, + * and a condition variable signal that finds a sleeper costs a kill(2) + * every time. So the publisher only signals when nobody is looking at + * the ring, and a worker that stops looking hands the ring over in its + * place. Between them the wakeup happens once per idle pool rather than + * once per block. + * + * scanners counts workers searching the ring, and deliberately not the + * ones inside a page read: a worker in a read cannot take new work, so + * counting it would let one busy worker silence the wakeups for a pool + * that is otherwise asleep. + * + * pending counts published requests nobody has claimed. It is what a + * searching worker reads instead of walking every slot, and what tells + * the process leaving the ring whether the ring still needs somebody. It + * is raised before its slot becomes visible, so it is never lower than + * the number of published slots and a claimer never takes it below zero; + * the two are equal only when no publication is in flight. + * + * sleepers decides nothing. It exists so that "the whole pool is asleep" + * is an observable fact from outside the pool. + */ + pg_atomic_uint32 scanners; + pg_atomic_uint32 pending; + pg_atomic_uint32 sleepers; + /* * Bumped under ReplayWarmReadLock whenever a relation or a database is * about to lose its files. A worker has no database connection and so @@ -144,12 +174,32 @@ typedef struct XLogWarmCtl */ pg_atomic_uint32 worker_pids[XLOGWARM_MAX_WORKERS]; + /* + * The slot each worker holds, one past its index, or zero for none. A + * request that stops moving belongs to somebody, and this is how the + * owner is found: a worker inside a read that never returns is the one + * case that costs the ring a slot for good, and without this the pid to + * look at is a guess. + */ + pg_atomic_uint32 worker_slots[XLOGWARM_MAX_WORKERS]; + XLogWarmSlot slots[FLEXIBLE_ARRAY_MEMBER]; } XLogWarmCtl; static XLogWarmCtl * XLogWarmQueue = NULL; +/* one claimed request, in the hands of the worker that claimed it */ +typedef struct XLogWarmRequest +{ + XLogWarmSlot *slot; + uint64 request_id; + RelFileLocator rlocator; + ForkNumber forknum; + BlockNumber blkno; +} XLogWarmRequest; + static void XLogWarmWorkerExit(int code, Datum arg); +static void XLogWarmHandOff(void); /* publisher-private state */ static uint64 next_request_id = 1; @@ -175,18 +225,15 @@ static uint64 my_drop_epoch = 0; * the relation" and re-measured for anything else. Direct-mapped and small * on purpose — replay works through a handful of relations at a time. */ -#define XLOGWARM_SIZES 16 - -typedef struct XLogWarmSize -{ - RelFileLocator rlocator; - ForkNumber forknum; - BlockNumber nblocks; - uint64 epoch; - bool valid; -} XLogWarmSize; - -static XLogWarmSize my_sizes[XLOGWARM_SIZES]; +/* + * How long a worker keeps searching an empty ring before it sleeps, in + * pg_spin_delay() rounds. It buys the publisher its silence: a worker that + * stays in the search over the gap between two publications is one the + * publisher does not have to wake, and at replay's rate those gaps are + * microseconds. Large enough to cover them, small enough that a standby + * with nothing to replay settles into sleeping workers within a moment. + */ +#define XLOGWARM_SPINS 1000 /* * Worker-private: the slot this worker holds, or -1. Read on the way out to @@ -195,6 +242,21 @@ static XLogWarmSize my_sizes[XLOGWARM_SIZES]; */ static int my_claimed_slot = -1; +/* + * Worker-private: which of the two shared counts this worker is part of. + * + * Both are read on the way out. A worker can be signalled away from either + * state, and a count left standing would say the pool has a searcher, or a + * sleeper, that no longer exists — the first silences the publisher's + * wakeups, the second makes "the whole pool is asleep" untrue where it is + * relied upon. + */ +static bool my_scanning = false; +static bool my_sleeping = false; + +/* Worker-private: this worker's index in the pool, or -1 outside one. */ +static int my_worker_id = -1; + Size XLogWarmShmemSize(void) { @@ -234,11 +296,17 @@ XLogWarmShmemInit(void) pg_atomic_init_u64(&XLogWarmQueue->vanished, 0); pg_atomic_init_u64(&XLogWarmQueue->discarded, 0); pg_atomic_init_u32(&XLogWarmQueue->hand, 0); + pg_atomic_init_u32(&XLogWarmQueue->scanners, 0); + pg_atomic_init_u32(&XLogWarmQueue->pending, 0); + pg_atomic_init_u32(&XLogWarmQueue->sleepers, 0); XLogWarmQueue->drop_epoch = 0; XLogWarmQueue->capacity = replay_warm_queue_size; for (int i = 0; i < XLOGWARM_MAX_WORKERS; i++) + { pg_atomic_init_u32(&XLogWarmQueue->worker_pids[i], 0); + pg_atomic_init_u32(&XLogWarmQueue->worker_slots[i], 0); + } for (int i = 0; i < replay_warm_queue_size; i++) pg_atomic_init_u32(&XLogWarmQueue->slots[i].state, XLOGWARM_FREE); @@ -309,6 +377,13 @@ XLogWarmPublish(RelFileLocator rlocator, ForkNumber forknum, slot->forknum = forknum; slot->blkno = blkno; + /* + * The count covers the slot before anybody else can see it. Raising it + * afterwards would let a worker claim the slot and lower a count that had + * not been raised yet, which on an unsigned counter is not a small error. + */ + pg_atomic_fetch_add_u32(&XLogWarmQueue->pending, 1); + /* the payload must be visible before a worker can see the state */ pg_write_barrier(); pg_atomic_write_u32(&slot->state, XLOGWARM_PUBLISHED); @@ -317,7 +392,18 @@ XLogWarmPublish(RelFileLocator rlocator, ForkNumber forknum, publish_hand = (slot_no + 1) % XLogWarmQueue->capacity; pg_atomic_fetch_add_u64(&XLogWarmQueue->published, 1); - ConditionVariableSignal(&XLogWarmQueue->cv_work); + /* + * Somebody already searching the ring will find this request without + * being told, so the signal — and the system call inside it — is only + * for a pool where nobody is. The barrier is what makes the two sides + * meet: a write barrier would order the publication, but not this load + * against it, and the pairing needs the load to come after. A worker on + * its way out of the search closes the other half of the window in + * XLogWarmHandOff(). + */ + pg_memory_barrier(); + if (pg_atomic_read_u32(&XLogWarmQueue->scanners) == 0) + ConditionVariableSignal(&XLogWarmQueue->cv_work); return slot_no; } @@ -390,7 +476,11 @@ XLogWarmCancelAll(void) if (pg_atomic_compare_exchange_u32(&slot->state, &expected, XLOGWARM_FREE)) + { + /* the request is gone, and so is the need for somebody to take it */ + pg_atomic_fetch_sub_u32(&XLogWarmQueue->pending, 1); pg_atomic_fetch_add_u64(&XLogWarmQueue->cancelled, 1); + } } publish_hand = 0; @@ -444,7 +534,7 @@ XLogWarmCountStale(void) * XLOGWARM_MAX_WORKERS entries, returning how many were found. */ int -XLogWarmGetWorkerPids(int *pids) +XLogWarmGetWorkerPids(int *pids, int *slots) { int found = 0; @@ -456,7 +546,12 @@ XLogWarmGetWorkerPids(int *pids) uint32 pid = pg_atomic_read_u32(&XLogWarmQueue->worker_pids[i]); if (pid != 0) + { + if (slots != NULL) + slots[found] = + (int) pg_atomic_read_u32(&XLogWarmQueue->worker_slots[i]) - 1; pids[found++] = (int) pid; + } } return found; @@ -504,6 +599,13 @@ XLogWarmGetSlotCounts(int *published, int *claimed) if (XLogWarmQueue == NULL) return; + /* + * This walk is not a snapshot: slots change state under it, so what it + * returns is what the ring looked like slot by slot rather than at any + * one instant. Good enough to see a request in flight, and not good + * enough to check a counter against while the pool is working. + */ + for (int i = 0; i < XLogWarmQueue->capacity; i++) { switch (pg_atomic_read_u32(&XLogWarmQueue->slots[i].state)) @@ -520,6 +622,28 @@ XLogWarmGetSlotCounts(int *published, int *claimed) } } +/* + * What the pool's processes are doing right now: how many are searching the + * ring, how many requests are waiting for one of them, and how many are + * asleep. Unlike the slot walk above these are single counters, so each is + * a real value rather than a scan; "the whole pool is asleep" is a fact a + * test can wait for here. + */ +void +XLogWarmGetPoolState(int *scanners, int *pending, int *sleepers) +{ + *scanners = 0; + *pending = 0; + *sleepers = 0; + + if (XLogWarmQueue == NULL) + return; + + *scanners = (int) pg_atomic_read_u32(&XLogWarmQueue->scanners); + *pending = (int) pg_atomic_read_u32(&XLogWarmQueue->pending); + *sleepers = (int) pg_atomic_read_u32(&XLogWarmQueue->sleepers); +} + /* * Read one published block into shared buffers. * @@ -532,7 +656,7 @@ XLogWarmDoOne(XLogWarmSlot * slot, uint64 request_id, RelFileLocator rlocator, ForkNumber forknum, BlockNumber blkno) { SMgrRelation smgr; - XLogWarmSize *size; + BlockNumber nblocks; Buffer buffer = InvalidBuffer; uint32 expected; bool failed = false; @@ -558,7 +682,6 @@ XLogWarmDoOne(XLogWarmSlot * slot, uint64 request_id, if (XLogWarmQueue->drop_epoch != my_drop_epoch) { smgrreleaseall(); - memset(my_sizes, 0, sizeof(my_sizes)); my_drop_epoch = XLogWarmQueue->drop_epoch; } @@ -570,41 +693,51 @@ XLogWarmDoOne(XLogWarmSlot * slot, uint64 request_id, * running ahead of it, and the outcome the interlock guarantees for a * request that gets here after a drop. * - * Asking outright costs more than it looks: smgrexists() closes the - * fork first (mdexists() skips that only in the startup process) and - * smgrnblocks() then walks the segment chain from the beginning, so - * on a terabyte relation one question is a thousand file opens. The - * answer is therefore remembered per epoch, and only the first - * request for a fork, or one that lands past a remembered end, pays - * for asking again. + * Asking the file system outright costs more than it looks: + * smgrexists() closes the fork before answering (mdexists() skips + * that only in the startup process) and the smgrnblocks() behind it + * then reopens the segment chain from the beginning, so on a terabyte + * relation one question is a thousand file opens. + * + * The size this worker last saw is therefore taken from the relation + * itself: smgrnblocks() records it there, smgrrelease() clears it, + * and the smgrreleaseall() above is what clears it after a drop. + * Reading the field directly is how the rest of the tree uses it — + * see the comment on smgrnblocks_cached(), whose InRecovery test is + * about the startup process and so never lets a worker in. */ - size = &my_sizes[rlocator.relNumber % XLOGWARM_SIZES]; + nblocks = smgr->smgr_cached_nblocks[forknum]; - if (!size->valid || size->epoch != my_drop_epoch || - size->forknum != forknum || - !RelFileLocatorEquals(size->rlocator, rlocator)) + if (nblocks == InvalidBlockNumber) { + /* + * Nothing known about this fork: either the worker has not + * touched it since the last drop, or it has never touched it at + * all. This is the one place that pays for the expensive + * question, and it is also the only place that can tell a + * relation whose files are gone from one that is merely shorter + * than the request expects. + */ if (!smgrexists(smgr, forknum)) { failed = true; pg_atomic_fetch_add_u64(&XLogWarmQueue->vanished, 1); } else - { - size->rlocator = rlocator; - size->forknum = forknum; - size->nblocks = smgrnblocks(smgr, forknum); - size->epoch = my_drop_epoch; - size->valid = true; - } + nblocks = smgrnblocks(smgr, forknum); } - if (!failed && blkno >= size->nblocks) + if (!failed && blkno >= nblocks) { - /* the remembered size may simply predate an extension */ - size->nblocks = smgrnblocks(smgr, forknum); + /* + * The remembered size may simply predate an extension: nothing + * tells a worker that replay has grown a relation, so a known + * size is a lower bound. mdnblocks() resumes from the last open + * segment, which makes this an lseek rather than another walk. + */ + nblocks = smgrnblocks(smgr, forknum); - if (blkno >= size->nblocks) + if (blkno >= nblocks) { failed = true; pg_atomic_fetch_add_u64(&XLogWarmQueue->vanished, 1); @@ -620,14 +753,21 @@ XLogWarmDoOne(XLogWarmSlot * slot, uint64 request_id, /* * Already resident: not a read, but still the answer replay * wants, so hand the buffer on as if we had read it. + * + * This is also where that question gets answered for + * pg_stat_recovery_prefetch. Replay used to ask it before + * publishing and count the answer itself; with the pool running + * it no longer asks, so the count belongs to whoever does. */ pg_atomic_fetch_add_u64(&XLogWarmQueue->hits, 1); + XLogPrefetchCountHit(); } else { buffer = ReadBufferWithoutRelcache(rlocator, forknum, blkno, RBM_NORMAL, NULL, true); pg_atomic_fetch_add_u64(&XLogWarmQueue->reads, 1); + XLogPrefetchCountPrefetch(); /* * Hand the buffer number on and let go: holding pins ahead of @@ -697,6 +837,34 @@ XLogWarmWorkerExit(int code, Datum arg) if (XLogWarmQueue == NULL) return; + /* + * Leave the pool's counts, and leave them in the order a live worker + * would. This callback runs in the before_shmem_exit phase, and the + * teardown that takes a process off a condition variable's wait list + * happens later, in ProcKill(): until then a publisher's signal can still + * land on this process, which is about to stop reading its latch. So the + * wait list goes first, then the counts, then the hand-off — a searcher + * that leaves without one takes the ring's only promised searcher with + * it. + */ + if (my_scanning || my_sleeping) + { + ConditionVariableCancelSleep(); + + if (my_sleeping) + { + pg_atomic_fetch_sub_u32(&XLogWarmQueue->sleepers, 1); + my_sleeping = false; + } + if (my_scanning) + { + pg_atomic_fetch_sub_u32(&XLogWarmQueue->scanners, 1); + my_scanning = false; + } + + XLogWarmHandOff(); + } + if (my_claimed_slot >= 0) { XLogWarmSlot *slot = &XLogWarmQueue->slots[my_claimed_slot]; @@ -709,56 +877,151 @@ XLogWarmWorkerExit(int code, Datum arg) pg_atomic_fetch_add_u64(&XLogWarmQueue->released, 1); } + pg_atomic_write_u32(&XLogWarmQueue->worker_slots[worker_id], 0); pg_atomic_write_u32(&XLogWarmQueue->worker_pids[worker_id], 0); } /* - * Claim and serve one published slot. Returns false when the ring holds - * nothing to do. + * Claim one published slot. Returns false when the ring holds nothing to do. + * + * The caller must be counted in scanners while this runs: that is what tells + * a publisher it need not spend a wakeup, and the promise behind it is that + * this process looks at the ring after the publication became visible. */ static bool -XLogWarmServeOne(void) +XLogWarmClaimOne(XLogWarmRequest * req) { int capacity = XLogWarmQueue->capacity; - uint32 start = pg_atomic_fetch_add_u32(&XLogWarmQueue->hand, 1); + uint32 start; + + /* + * No request outstanding, and the counter says so without touching a + * slot. A worker that searched the whole ring every time it looked would + * spend the pool's cores dragging several hundred shared cache lines + * between them, which is what makes waiting here cheap enough to prefer + * to sleeping. + */ + if (pg_atomic_read_u32(&XLogWarmQueue->pending) == 0) + return false; + + start = pg_atomic_fetch_add_u32(&XLogWarmQueue->hand, 1); for (int i = 0; i < capacity; i++) { XLogWarmSlot *slot = &XLogWarmQueue->slots[(start + i) % capacity]; uint32 expected = XLOGWARM_PUBLISHED; - uint64 request_id; - RelFileLocator rlocator; - ForkNumber forknum; - BlockNumber blkno; if (!pg_atomic_compare_exchange_u32(&slot->state, &expected, XLOGWARM_CLAIMED)) continue; + pg_atomic_fetch_sub_u32(&XLogWarmQueue->pending, 1); + /* the state was observed before the payload it advertises */ pg_read_barrier(); - request_id = slot->request_id; - rlocator = slot->rlocator; - forknum = slot->forknum; - blkno = slot->blkno; + req->slot = slot; + req->request_id = slot->request_id; + req->rlocator = slot->rlocator; + req->forknum = slot->forknum; + req->blkno = slot->blkno; /* * From here until the slot is finished this worker owns it, and says * so where its exit callback can see it. */ my_claimed_slot = (start + i) % capacity; + if (my_worker_id >= 0) + pg_atomic_write_u32(&XLogWarmQueue->worker_slots[my_worker_id], + (uint32) my_claimed_slot + 1); pg_atomic_fetch_add_u64(&XLogWarmQueue->claimed, 1); - XLogWarmDoOne(slot, request_id, rlocator, forknum, blkno); - - my_claimed_slot = -1; return true; } return false; } +/* + * Hand the ring over on the way out of the search. + * + * A publisher that saw this process searching stayed quiet, so a process + * that stops searching — to read a page, or for good — has to make sure + * somebody else is looking if anything is still outstanding. Otherwise the + * work it was trusted to find would sit in front of a sleeping pool until + * the next publication happened to wake somebody. + * + * Two things must already be true at the call: this process has left the + * scanners count, and it is not itself on the wait list — a signal issued + * while still registered could pick the signaller and leave the others + * asleep. + */ +static void +XLogWarmHandOff(void) +{ + /* + * Pairs with the publisher: it publishes and then reads scanners, this + * side leaves scanners and then reads pending, and a full barrier on both + * sides is what guarantees at least one of the two sees the other. + */ + pg_memory_barrier(); + + if (pg_atomic_read_u32(&XLogWarmQueue->pending) > 0 && + pg_atomic_read_u32(&XLogWarmQueue->scanners) == 0) + ConditionVariableSignal(&XLogWarmQueue->cv_work); +} + +/* + * Stop and start searching, as the states above are entered and left. + */ +static void +XLogWarmStopScanning(void) +{ + Assert(my_scanning); + pg_atomic_fetch_sub_u32(&XLogWarmQueue->scanners, 1); + my_scanning = false; + XLogWarmHandOff(); +} + +static void +XLogWarmStartScanning(void) +{ + Assert(!my_scanning); + pg_atomic_fetch_add_u32(&XLogWarmQueue->scanners, 1); + my_scanning = true; +} + +/* + * Serve a claimed request and go back to searching. + */ +static void +XLogWarmServe(XLogWarmRequest * req) +{ + XLogWarmDoOne(req->slot, req->request_id, req->rlocator, req->forknum, + req->blkno); + my_claimed_slot = -1; + if (my_worker_id >= 0) + pg_atomic_write_u32(&XLogWarmQueue->worker_slots[my_worker_id], 0); +} + +/* + * Leave the search to serve what this worker just claimed, then rejoin it. + * + * The injection point catches a worker in the state the hand-off exists for: + * holding a request, and still counted as a searcher, so a publication + * landing now is one the publisher will leave to this process. It is safe + * to park here — the worker is not on the pool's wait list at this point, so + * the waiting the injection point does of its own cannot disturb it. + */ +static void +XLogWarmServeAsScanner(XLogWarmRequest * req) +{ + INJECTION_POINT("replay-warm-claimed", NULL); + XLogWarmStopScanning(); + XLogWarmServe(req); + XLogWarmStartScanning(); +} + /* * Register the pool. Like the DWB cleaner pool, a worker slot shortage is * fatal rather than silent: a smaller pool than the operator configured is @@ -834,11 +1097,18 @@ XLogWarmWorkerMain(Datum main_arg) worker_id = DatumGetInt32(main_arg); Assert(worker_id >= 0 && worker_id < XLOGWARM_MAX_WORKERS); + my_worker_id = worker_id; pg_atomic_write_u32(&XLogWarmQueue->worker_pids[worker_id], MyProcPid); before_shmem_exit(XLogWarmWorkerExit, Int32GetDatum(worker_id)); + /* this process is searching the ring from here on */ + XLogWarmStartScanning(); + for (;;) { + XLogWarmRequest req; + int spins; + /* the CFI is what turns a pending die() into the FATAL exit */ CHECK_FOR_INTERRUPTS(); @@ -848,24 +1118,72 @@ XLogWarmWorkerMain(Datum main_arg) ProcessConfigFile(PGC_SIGHUP); } - if (!XLogWarmServeOne()) + if (XLogWarmClaimOne(&req)) { - /* - * Sleep without losing a wakeup: get onto the wait list first, - * then recheck, then sleep. A signal sent after the recheck is - * kept by the prepared state; a request published before it is - * seen by the recheck. - */ - ConditionVariablePrepareToSleep(&XLogWarmQueue->cv_work); - if (!XLogWarmServeOne()) - { - ConditionVariableSleep(&XLogWarmQueue->cv_work, - WAIT_EVENT_REPLAY_WARM_MAIN); - continue; - } + XLogWarmServeAsScanner(&req); + continue; } - /* off the wait list while serving (no-op if never prepared) */ + /* + * Nothing to do this instant, which at replay's publication rate + * usually means "not yet" rather than "not at all". Stay in the + * search for a while: a worker that is still counted is a worker the + * publisher does not have to wake, and the whole point of the counts + * is to keep that system call out of replay's way. The budget is + * small enough that an idle standby settles into sleeping workers + * rather than spinning ones. + */ + for (spins = XLOGWARM_SPINS; spins > 0; spins--) + { + if (pg_atomic_read_u32(&XLogWarmQueue->pending) > 0) + break; + pg_spin_delay(); + } + + if (spins > 0 && XLogWarmClaimOne(&req)) + { + XLogWarmServeAsScanner(&req); + continue; + } + + /* + * Give up and sleep, without losing a wakeup: join the wait list + * first, then leave the searchers, then recheck. In that order a + * publisher that reads no searchers is reading about a process that + * is already waiting, and a publisher that reads one is reading about + * a process that has yet to look again. + */ + ConditionVariablePrepareToSleep(&XLogWarmQueue->cv_work); + pg_atomic_fetch_sub_u32(&XLogWarmQueue->scanners, 1); + my_scanning = false; + + if (XLogWarmClaimOne(&req)) + { + /* leave the wait list before the hand-off can pick this process */ + ConditionVariableCancelSleep(); + XLogWarmHandOff(); + XLogWarmServe(&req); + XLogWarmStartScanning(); + continue; + } + + pg_atomic_fetch_add_u32(&XLogWarmQueue->sleepers, 1); + my_sleeping = true; + + ConditionVariableSleep(&XLogWarmQueue->cv_work, + WAIT_EVENT_REPLAY_WARM_MAIN); + + /* + * ConditionVariableSleep() puts this process back on the wait list + * before it returns, so become a searcher while still registered: + * between the two counts there must be no moment where this process + * is neither searching nor waiting, or a publisher could look at that + * moment and decide the ring needs nobody. + */ + pg_atomic_fetch_sub_u32(&XLogWarmQueue->sleepers, 1); + my_sleeping = false; + pg_atomic_fetch_add_u32(&XLogWarmQueue->scanners, 1); + my_scanning = true; ConditionVariableCancelSleep(); } } diff --git a/src/include/access/xlogprefetcher.h b/src/include/access/xlogprefetcher.h index 50b39c1fb0d77..a7e55dcbe61f7 100644 --- a/src/include/access/xlogprefetcher.h +++ b/src/include/access/xlogprefetcher.h @@ -39,6 +39,17 @@ extern void XLogPrefetchShmemInit(void); extern void XLogPrefetchResetStats(void); +/* + * Counting from outside the startup process. + * + * The replay warm pool decides in its workers what replay used to decide for + * itself — whether a block was already in a buffer — so the two counters that + * record that decision are incremented from there. They keep their meaning; + * only the process holding the answer has changed. + */ +extern void XLogPrefetchCountHit(void); +extern void XLogPrefetchCountPrefetch(void); + extern XLogPrefetcher *XLogPrefetcherAllocate(XLogReaderState *reader); extern void XLogPrefetcherFree(XLogPrefetcher *prefetcher); diff --git a/src/include/access/xlogwarm.h b/src/include/access/xlogwarm.h index a68ceca293a3c..e7b45b76f3e57 100644 --- a/src/include/access/xlogwarm.h +++ b/src/include/access/xlogwarm.h @@ -54,7 +54,8 @@ extern void XLogWarmDropBegin(void); extern void XLogWarmDropEnd(void); extern bool XLogWarmGetStats(XLogWarmStats * stats); extern void XLogWarmGetSlotCounts(int *published, int *claimed); -extern int XLogWarmGetWorkerPids(int *pids); +extern void XLogWarmGetPoolState(int *scanners, int *pending, int *sleepers); +extern int XLogWarmGetWorkerPids(int *pids, int *slots); /* the pool size ceiling, matching the setting's maximum */ #define XLOGWARM_MAX_WORKERS 64 diff --git a/src/test/modules/test_dwb/t/021_replay_warm.pl b/src/test/modules/test_dwb/t/021_replay_warm.pl index 4d8e4ac047296..c391aec262b5b 100644 --- a/src/test/modules/test_dwb/t/021_replay_warm.pl +++ b/src/test/modules/test_dwb/t/021_replay_warm.pl @@ -395,6 +395,249 @@ sub startup_reads pass('the pool restored its worker'); } +# --- the wakeup protocol ------------------------------------------------ + +# Replay publishes on the order of a hundred thousand blocks a second, and +# waking a worker for each of them costs a system call each time. So the +# publisher stays quiet while somebody is searching the ring, and a worker +# that stops searching hands the ring on in its place. Driving that from +# replay would mean driving it thousands of requests at a time; these +# scenarios hand the pool one request at a time instead. + +my $proto = make_standby('warm_proto', 2); +$proto->start; +$proto->poll_query_until('postgres', + 'SELECT count(*) = 2 FROM test_dwb_warm_worker_pids()') + or die 'timed out waiting for the protocol standby to start its workers'; +$primary->wait_for_catchup($proto, 'replay'); + +sub pool_state +{ + my ($node) = @_; + my %s; + @s{qw(published claimed scanners pending sleepers)} = split /\|/, + $node->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_slot_states()'); + return \%s; +} + +# With nothing being replayed the pool has nothing to search for, and the +# spin that keeps a busy worker out of the wait list runs out. +$proto->poll_query_until('postgres', + 'SELECT sleepers = 2 AND scanners = 0 FROM test_dwb_warm_slot_states()') + or die 'the idle pool never settled into sleeping workers'; +pass('an idle pool settles into sleeping workers'); + +# Quiesced, the count and the ring agree. Under load they need not: a +# publication raises the count before its slot becomes visible, so the count +# leads by whatever is in flight, and the ring walk is not a snapshot anyway. +my $quiet = pool_state($proto); +is($quiet->{pending}, $quiet->{published}, + 'the pending count matches the published slots with the pool quiesced'); + +# The edge a lost wakeup would show at: a request arriving at a pool where +# nobody is searching has to be one the publisher wakes somebody for. +my $idle_before = warm_counters($proto); +my $pf_before = $proto->safe_psql('postgres', + q{SELECT prefetch || ' ' || hit FROM pg_stat_recovery_prefetch}); +my ($pf_prefetch, $pf_hit) = split / /, $pf_before; + +my $slot = + $proto->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', 0)"); +cmp_ok($slot, '>=', 0, 'the published request got a slot'); +$proto->poll_query_until('postgres', + "SELECT claimed > $idle_before->{claimed} FROM test_dwb_warm_counters()") + or die 'a request published to a sleeping pool was never claimed'; +pass('a request published to a sleeping pool is served'); + +# Whether a block was already in a buffer is decided in the pool now, and +# pg_stat_recovery_prefetch is where that decision has always been counted. +# The block above was not resident, so it was read; asking for the same block +# again is the other answer. +$proto->poll_query_until('postgres', + "SELECT prefetch > $pf_prefetch FROM pg_stat_recovery_prefetch") + or die 'the view did not count the page the pool read'; +pass('the view counts a page the pool read'); + +$proto->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', 0)"); +$proto->poll_query_until('postgres', + "SELECT hit > $pf_hit FROM pg_stat_recovery_prefetch") + or die 'the view did not count the page the pool found resident'; +pass('the view counts a page the pool found already in a buffer'); + +# A worker can be signalled away while it sleeps, and the count it is part of +# has to go with it: the decrement that follows the sleep never runs in that +# case, and a count left standing says the pool has a sleeper it does not +# have — which is the very fact the scenarios above wait on. +$proto->poll_query_until('postgres', + 'SELECT sleepers = 2 FROM test_dwb_warm_slot_states()') + or die 'the pool did not settle before the worker was killed; state: ' + . $proto->safe_psql('postgres', 'SELECT * FROM test_dwb_warm_slot_states()') + . ' workers: ' + . $proto->safe_psql( + 'postgres', + q{SELECT string_agg(worker || ':' || pid || ':' || holding, ',') + FROM test_dwb_warm_worker_pids()}); + +my $doomed = $proto->safe_psql('postgres', + 'SELECT pid FROM test_dwb_warm_worker_pids() ORDER BY worker LIMIT 1'); +kill 'TERM', $doomed; + +$proto->poll_query_until('postgres', + 'SELECT count(*) = 2 FROM test_dwb_warm_worker_pids()') + or die 'the protocol standby did not restore its worker'; +$proto->poll_query_until('postgres', + 'SELECT sleepers = 2 FROM test_dwb_warm_slot_states()') + or die 'a worker killed in its sleep left its count behind; state: ' + . $proto->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_slot_states()'); +pass('a worker killed in its sleep leaves no count behind'); + +# The pool still works afterwards, which is what the counts are for. +my $after_kill = warm_counters($proto); +$proto->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', 4)"); +$proto->poll_query_until('postgres', + "SELECT claimed > $after_kill->{claimed} FROM test_dwb_warm_counters()") + or die 'the pool stopped serving after losing a sleeping worker'; +pass('the pool serves again after losing a sleeping worker'); + +SKIP: +{ + skip 'injection points not supported by this build', 3 + unless $injection_points; + + # Park a worker where it holds a request and still counts as a searcher. + # Everything published while it sits there is something the publisher + # leaves to it, so the only way the rest of the ring gets served is if + # that worker hands the ring on when it stops searching. + $proto->safe_psql('postgres', + "SELECT injection_points_attach('replay-warm-claimed', 'wait')"); + $proto->poll_query_until('postgres', + 'SELECT sleepers = 2 FROM test_dwb_warm_slot_states()') + or die 'the pool did not go back to sleep before the hand-off scenario'; + + $proto->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', 1)"); + $proto->poll_query_until( + 'postgres', + 'SELECT scanners = 1 AND sleepers = 1 AND claimed = 1 + FROM test_dwb_warm_slot_states()') + or die 'no worker parked holding a request; state: ' + . $proto->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_slot_states()'); + pass('a worker parks holding a request, still counted as a searcher'); + + $proto->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', 2)"); + $proto->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', 3)"); + + # Two requests, one searcher the publisher trusted, and a worker asleep + # that was told nothing. + is( $proto->safe_psql( + 'postgres', 'SELECT pending FROM test_dwb_warm_slot_states()'), + 2, + 'requests wait while the publisher leaves them to the searcher'); + + # Let the parked worker go, but leave the point attached: it takes one + # of the two waiting requests and parks again. The other one can only + # be served by the worker that is asleep, and nothing has woken it but + # the hand-off. + $proto->safe_psql('postgres', + "SELECT injection_points_wakeup('replay-warm-claimed')"); + $proto->poll_query_until('postgres', + 'SELECT pending = 0 FROM test_dwb_warm_slot_states()') + or die 'the worker leaving the search did not hand the ring on; state: ' + . $proto->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_slot_states()'); + pass('a worker leaving the search hands the ring to a sleeping one'); + + # Let the parked workers out and put the pool back to sleep before the + # next scenario builds its own state. + $proto->safe_psql('postgres', + "SELECT injection_points_detach('replay-warm-claimed')"); + # "Nobody is holding a request" is not the same as "nobody is parked": + # a detached point can still be reached by a worker that looked it up a + # moment earlier, so a loop that stops at the first idle instant can + # leave the next claim parked with nothing left to wake it. Waiting for + # the pool to be asleep is the state that cannot be a gap between two + # claims. + foreach my $attempt (1 .. 600) + { + last + if $proto->safe_psql( + 'postgres', + 'SELECT claimed = 0 AND sleepers = 2 + FROM test_dwb_warm_slot_states()') eq 't'; + $proto->psql('postgres', + "SELECT injection_points_wakeup('replay-warm-claimed')"); + usleep(100_000); + } + $proto->poll_query_until('postgres', + 'SELECT claimed = 0 AND sleepers = 2 FROM test_dwb_warm_slot_states()' + ) + or die 'workers stayed parked after the point was detached; state: ' + . $proto->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_slot_states()'); + $proto->poll_query_until('postgres', + 'SELECT sleepers = 2 AND pending = 0 FROM test_dwb_warm_slot_states()' + ) or die 'the pool did not settle before the dying-searcher scenario'; + + # The same hand-off from the other side: the searcher the publisher + # trusted does not stop searching, it dies. Whatever it was trusted to + # find has to be picked up by the worker asleep beside it, and the only + # thing that can tell that worker is the exit path. + # + # The point stays attached on purpose. The dying worker is replaced + # within a second, and a replacement free to drain the ring would hide a + # missing hand-off: it parks on its first claim instead, so of the two + # requests waiting it can take only one, and the other is left where + # nothing but the hand-off reaches it. + $proto->safe_psql('postgres', + "SELECT injection_points_attach('replay-warm-claimed', 'wait')"); + $proto->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', 5)"); + $proto->poll_query_until('postgres', + 'SELECT scanners = 1 AND claimed = 1 FROM test_dwb_warm_slot_states()' + ) or die 'no worker parked for the dying-searcher scenario'; + + $proto->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', 6)"); + $proto->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', 7)"); + + # The workers advertise which slot each of them holds, so the one to end + # is the one parked rather than the one asleep. + my $victim = $proto->safe_psql('postgres', + 'SELECT pid FROM test_dwb_warm_worker_pids() WHERE holding >= 0'); + like($victim, qr/^\d+$/, 'exactly one worker is holding a request'); + kill 'TERM', $victim; + + $proto->poll_query_until('postgres', + 'SELECT pending = 0 FROM test_dwb_warm_slot_states()') + or die 'a searcher that died did not hand the ring on; state: ' + . $proto->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_slot_states()'); + pass('a searcher that dies hands the ring to a sleeping worker'); + + # Nobody is woken from here on, and nothing below needs a worker to + # move. The worker killed above was waiting at the point, and a waiter + # that dies leaves its registration behind: a wakeup goes to the first + # registration under that name, so it would keep going to a process that + # no longer exists. This node is finished after the check below. + $proto->safe_psql('postgres', + "SELECT injection_points_detach('replay-warm-claimed')"); + + # A worker died holding a request, which is one of the two ways a + # published request leaves the ring without a claim behind it. + $proto->poll_query_until('postgres', + 'SELECT count(*) = 2 FROM test_dwb_warm_worker_pids()') + or die 'the pool did not come back after the dying-searcher scenario'; + $proto->poll_query_until('postgres', + 'SELECT pending = published FROM test_dwb_warm_slot_states()') + or die 'the pending count and the ring disagree after a worker died ' + . 'holding a request; state: ' + . $proto->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_slot_states()'); + pass('the pending count survives a worker dying with a request'); +} + +$proto->stop; + # --- the pool on its own, with kernel advice turned off ----------------- my $noadvice = @@ -413,6 +656,59 @@ sub startup_reads 'replay collects the pool answers with the advice prefetcher off'); $noadvice->stop; +# --- a relation that grew after a worker learned its size --------------- + +# The size a worker goes by is the one the storage manager recorded when it +# last asked, and nothing tells a worker that replay has extended a +# relation. A request past the end it knows must therefore make it ask +# again rather than refuse on what it remembers. +# +# The remembered size belongs to the process that asked, so this runs with +# a single worker: with more of them the second half could land in a +# process that never saw the first and would pass without asking anything. +my $onework = make_standby('warm_onework', 1); +$onework->start; +$onework->poll_query_until('postgres', + 'SELECT count(*) = 1 FROM test_dwb_warm_worker_pids()') + or die 'timed out waiting for the single warm worker to start'; +$primary->wait_for_catchup($onework, 'replay'); + +my $past = $onework->safe_psql('postgres', + q{SELECT (pg_relation_size('t') / current_setting('block_size')::int)::int} +); + +my $before = warm_counters($onework); +$onework->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', $past)"); +$onework->poll_query_until('postgres', + "SELECT vanished > $before->{vanished} FROM test_dwb_warm_counters()") + or die 'a request past the end of a relation was not refused'; +pass('a block past the end of a relation is refused'); + +$primary->safe_psql('postgres', + q{INSERT INTO t SELECT g, repeat('z', 200) FROM generate_series(40001, 60000) g} +); +$primary->wait_for_catchup($onework, 'replay'); + +cmp_ok( + $onework->safe_psql( + 'postgres', + q{SELECT (pg_relation_size('t') / current_setting('block_size')::int)::int} + ), + '>', $past, + 'the relation grew past the refused block'); + +$before = warm_counters($onework); +$onework->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', $past)"); +$onework->poll_query_until( + 'postgres', + "SELECT hits + reads > @{[ $before->{hits} + $before->{reads} ]} + FROM test_dwb_warm_counters()") + or die 'the worker went by the size it learned before the relation grew; ' + . 'counters: ' + . $onework->safe_psql('postgres', 'SELECT * FROM test_dwb_warm_counters()'); +pass('a worker asks again for a block past the size it knows'); +$onework->stop; + # --- promotion with requests still outstanding -------------------------- SKIP: @@ -460,6 +756,18 @@ sub startup_reads '>', 0, 'the promoted node has the replayed data'); +# The end of recovery withdraws every request nobody claimed, which is the +# other way the count of outstanding requests goes down without a claim +# behind it. A withdrawal that forgot the count would leave the pool +# claiming to owe work it has thrown away. +$standby->poll_query_until('postgres', + 'SELECT pending = published FROM test_dwb_warm_slot_states()') + or die 'the pending count and the ring disagree after promotion withdrew ' + . 'the outstanding requests; state: ' + . $standby->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_slot_states()'); +pass('the pending count comes down with the requests promotion withdraws'); + $standby->stop; $primary->stop; diff --git a/src/test/modules/test_dwb/test_dwb--1.0.sql b/src/test/modules/test_dwb/test_dwb--1.0.sql index 686f8a9834bb4..5edc062d25d02 100644 --- a/src/test/modules/test_dwb/test_dwb--1.0.sql +++ b/src/test/modules/test_dwb/test_dwb--1.0.sql @@ -125,11 +125,17 @@ CREATE FUNCTION test_dwb_count_rel_buffers(relnumber oid) AS 'MODULE_PATHNAME' LANGUAGE C; CREATE FUNCTION test_dwb_warm_slot_states( - OUT published int, OUT claimed int) + OUT published int, OUT claimed int, + OUT scanners int, OUT pending int, OUT sleepers int) RETURNS record STRICT AS 'MODULE_PATHNAME' LANGUAGE C; -CREATE FUNCTION test_dwb_warm_worker_pids(OUT worker int, OUT pid int) +CREATE FUNCTION test_dwb_warm_publish(rel regclass, blkno int) + RETURNS int STRICT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dwb_warm_worker_pids( + OUT worker int, OUT pid int, OUT holding int) RETURNS SETOF record AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index ce9e3f7c19325..99eccba2ecfe8 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -1246,9 +1246,11 @@ test_dwb_warm_counters(PG_FUNCTION_ARGS) } /* - * Pids of the running warm workers. They hold no database connection, so - * pg_stat_activity cannot show them; this is how a test finds one to kill - * and how an operator sees the pool is alive. + * Pids of the running warm workers, and the slot each one holds. They keep + * no database connection, so pg_stat_activity cannot show them; this is how + * a test finds one to kill, how it finds the one holding a given request, + * and how an operator sees the pool is alive. A worker holding nothing + * reports -1. */ PG_FUNCTION_INFO_V1(test_dwb_warm_worker_pids); Datum @@ -1256,18 +1258,20 @@ test_dwb_warm_worker_pids(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; int pids[XLOGWARM_MAX_WORKERS]; + int slots[XLOGWARM_MAX_WORKERS]; int nworkers; InitMaterializedSRF(fcinfo, 0); - nworkers = XLogWarmGetWorkerPids(pids); + nworkers = XLogWarmGetWorkerPids(pids, slots); for (int i = 0; i < nworkers; i++) { - Datum values[2]; - bool nulls[2] = {0}; + Datum values[3]; + bool nulls[3] = {0}; values[0] = Int32GetDatum(i); values[1] = Int32GetDatum(pids[i]); + values[2] = Int32GetDatum(slots[i]); tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); } @@ -1275,27 +1279,39 @@ test_dwb_warm_worker_pids(PG_FUNCTION_ARGS) } /* - * What the pool is doing right now: requests waiting for a worker, and - * requests a worker holds. The running totals say what has happened; this - * is what a test needs to catch a request in flight. + * What the pool is doing right now: requests waiting for a worker, requests + * a worker holds, and what the workers themselves are up to. The running + * totals say what has happened; this is what a test needs to catch a request + * in flight, or to wait until the whole pool is asleep. + * + * The first two come from a walk of the ring, which slots change state + * under, so they are an impression rather than an instant. The last three + * are counters and are exact. */ PG_FUNCTION_INFO_V1(test_dwb_warm_slot_states); Datum test_dwb_warm_slot_states(PG_FUNCTION_ARGS) { TupleDesc tupdesc; - Datum values[2]; - bool nulls[2] = {0}; + Datum values[5]; + bool nulls[5] = {0}; int published; int claimed; + int scanners; + int pending; + int sleepers; if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); XLogWarmGetSlotCounts(&published, &claimed); + XLogWarmGetPoolState(&scanners, &pending, &sleepers); values[0] = Int32GetDatum(published); values[1] = Int32GetDatum(claimed); + values[2] = Int32GetDatum(scanners); + values[3] = Int32GetDatum(pending); + values[4] = Int32GetDatum(sleepers); PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); } @@ -1379,6 +1395,44 @@ test_dwb_pin_xact_callback(XactEvent event, void *arg) } } +/* + * Hand the pool one request, from here instead of from replay. + * + * The pool's wakeup protocol is about what happens between a publication and + * the workers, and driving it from replay means driving it from a stream of + * thousands a second — nothing a test can aim. This publishes exactly one + * request, so a test can set the pool up in a known state and then watch what + * a single block does to it. Meant for a pool that is otherwise idle: the + * publisher's slot cursor and request ids are per-process, so a second + * publisher alongside a busy replay would be publishing into the same ring + * with a cursor of its own. + */ +PG_FUNCTION_INFO_V1(test_dwb_warm_publish); +Datum +test_dwb_warm_publish(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + BlockNumber blkno = (BlockNumber) PG_GETARG_INT32(1); + Relation rel; + RelFileLocator rlocator; + uint64 request_id; + int slot_no; + + if (!XLogWarmPoolActive()) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("the replay warm pool is not configured"), + errhint("Set \"replay_warm_workers\" above 0."))); + + rel = relation_open(relid, AccessShareLock); + rlocator = rel->rd_locator; + relation_close(rel, NoLock); + + slot_no = XLogWarmPublish(rlocator, MAIN_FORKNUM, blkno, &request_id); + + PG_RETURN_INT32(slot_no); +} + PG_FUNCTION_INFO_V1(test_dwb_pin_block); Datum test_dwb_pin_block(PG_FUNCTION_ARGS) From c102cc0e2c4f15b3303b39541f5ba312d7e3823b Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 7 Aug 2026 10:44:09 +0300 Subject: [PATCH 46/52] Widen the replay warm ring to what the measurements ask for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ring bounds how far ahead of replay the pool looks, and since every block reference now takes a slot rather than only the misses, the shipped 256 buys a quarter of the read-ahead it used to. Four points on a two-host pair, one after another so the pair ages equally between them, 600 s of 750-client pgbench each against a synchronous standby: ring applied pool CPU tps 256 38.8 MB/s 5.9 cores 136.0k 512 47.8 MB/s 9.7 cores 141.2k 1024 47.0 MB/s 14.3 cores 137.1k 2048 42.7 MB/s 13.3 cores 139.0k 512 is where it stops paying: 1024 buys nothing for half again as much pool CPU, and at 1024 the startup process's wait for pages falls from 15.3 % to 10.3 % without moving throughput, which says the wall past this point is the single replay process itself and not the window. Also adds the scenario for the race the exit-path hand-off exists to close: the worker that dies is the one the publisher has just woken. A signal goes to the head of the wait list and takes it off, so a head on its way out consumes the only wakeup. The head is made certain by replacing one of two workers — the list is joined at the tail — and the race is held open by stopping that process, which keeps it registered and asleep where an injection point could not: preparing to sleep on the point's own variable cancels the registration on the pool's. The evidence is which worker ends up holding the slot, not that the slot was served, because the replacement of the dead worker would serve it either way. --- doc/src/sgml/config.sgml | 2 +- src/backend/access/transam/xlogwarm.c | 2 +- src/backend/utils/misc/guc_tables.c | 2 +- src/backend/utils/misc/postgresql.conf.sample | 2 +- .../modules/test_dwb/t/021_replay_warm.pl | 104 ++++++++++++++++++ 5 files changed, 108 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index a9430b17daccc..51dff8b2280e0 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -4443,7 +4443,7 @@ include_dir 'conf.d' How many block requests the pool enabled by can hold at once, which also bounds how far ahead of replay the server looks for blocks to warm. - The default is 256. This parameter can only be set at server start. + The default is 512. This parameter can only be set at server start. Every block a record refers to takes a request, whether or not the diff --git a/src/backend/access/transam/xlogwarm.c b/src/backend/access/transam/xlogwarm.c index de209535a9181..3eb1a651ac414 100644 --- a/src/backend/access/transam/xlogwarm.c +++ b/src/backend/access/transam/xlogwarm.c @@ -78,7 +78,7 @@ /* GUCs */ int replay_warm_workers = 0; -int replay_warm_queue_size = 256; +int replay_warm_queue_size = 512; typedef enum XLogWarmState diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 20c8f8bf4fe07..ffea9f1f6ca7c 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -2261,7 +2261,7 @@ struct config_int ConfigureNamesInt[] = "prefetcher looks when the pool is enabled.") }, &replay_warm_queue_size, - 256, 16, 8192, + 512, 16, 8192, NULL, NULL, NULL }, { diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 2761f74c1d157..709012b839d75 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -300,7 +300,7 @@ # (change requires restart) #replay_warm_workers = 0 # workers reading pages ahead of replay # (change requires restart) -#replay_warm_queue_size = 256 # block requests the warm pool can hold +#replay_warm_queue_size = 512 # block requests the warm pool can hold # (change requires restart) # - Archiving - diff --git a/src/test/modules/test_dwb/t/021_replay_warm.pl b/src/test/modules/test_dwb/t/021_replay_warm.pl index c391aec262b5b..fd1a0988767c1 100644 --- a/src/test/modules/test_dwb/t/021_replay_warm.pl +++ b/src/test/modules/test_dwb/t/021_replay_warm.pl @@ -501,6 +501,110 @@ sub pool_state or die 'the pool stopped serving after losing a sleeping worker'; pass('the pool serves again after losing a sleeping worker'); +SKIP: +{ + skip 'injection points not supported by this build', 2 + unless $injection_points; + + # The worse version of the same thing: the worker that dies is the one + # the publisher has just woken. A signal is delivered to the head of the + # wait list and takes it off that list, so if the head is on its way out, + # the request it was woken for has been told to nobody. Handing the ring + # on from the exit path is what covers that, and it is the only thing + # that can. + # + # Freezing the process is what makes the race observable. A stopped + # worker stays on the wait list and stays asleep, so the wakeup is spent + # on it and the rest of the pool hears nothing. An injection point could + # not stand in for this: preparing to sleep on the point's own variable + # cancels the registration on the pool's — see + # ConditionVariablePrepareToSleep — and that registration is the whole + # subject. + $proto->poll_query_until('postgres', + 'SELECT sleepers = 2 AND pending = 0 FROM test_dwb_warm_slot_states()' + ) or die 'the pool did not settle before the head-of-queue scenario'; + + my @pids = split /\n/, + $proto->safe_psql('postgres', + 'SELECT pid FROM test_dwb_warm_worker_pids() ORDER BY worker'); + + # The list is joined at the tail and served from the head, so replacing + # one of the two workers leaves the other at the head for certain: + # whatever the order was, the replacement can only have joined behind it. + kill 'TERM', $pids[1]; + $proto->poll_query_until( + 'postgres', + "SELECT count(*) = 2 AND count(*) FILTER (WHERE pid = $pids[1]) = 0 + FROM test_dwb_warm_worker_pids()" + ) or die 'the protocol standby did not replace the worker'; + $proto->poll_query_until('postgres', + 'SELECT sleepers = 2 FROM test_dwb_warm_slot_states()') + or die 'the pool did not settle after the worker was replaced'; + + my $head = $pids[0]; + my $rest = $proto->safe_psql('postgres', + "SELECT pid FROM test_dwb_warm_worker_pids() WHERE pid <> $head"); + + # The dying worker is replaced within a second, and a replacement is free + # to walk the ring and take whatever it finds — which would drain the + # request whether or not anybody was ever told about it. So the point + # stays attached: the worker that claims parks while still holding the + # slot, and the pool then says which pid that is. A served request is + # not the evidence here; who served it is. + $proto->safe_psql('postgres', + "SELECT injection_points_attach('replay-warm-claimed', 'wait')"); + + kill 'STOP', $head; + + $proto->safe_psql('postgres', "SELECT test_dwb_warm_publish('t', 6)"); + is( $proto->safe_psql( + 'postgres', 'SELECT pending FROM test_dwb_warm_slot_states()'), + 1, + 'the one wakeup goes to the worker at the head of the wait list'); + + # That worker never gets to serve it: it is signalled away before it runs + # again, so the request now depends entirely on what its exit path does. + kill 'TERM', $head; + kill 'CONT', $head; + + $proto->poll_query_until('postgres', + 'SELECT count(*) = 1 FROM test_dwb_warm_worker_pids() WHERE holding >= 0' + ) + or die 'nobody took the request the dying worker was woken for; state: ' + . $proto->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_slot_states()'); + is( $proto->safe_psql( + 'postgres', + 'SELECT pid FROM test_dwb_warm_worker_pids() WHERE holding >= 0'), + $rest, + 'a worker that dies holding the wakeup hands the ring to the sleeper' + ); + + # Let the parked worker out and put the pool back to sleep, the same way + # the scenarios below do: a detached point can still be reached by a + # worker that looked it up a moment earlier. + $proto->safe_psql('postgres', + "SELECT injection_points_detach('replay-warm-claimed')"); + foreach my $attempt (1 .. 600) + { + last + if $proto->safe_psql( + 'postgres', + 'SELECT claimed = 0 AND sleepers = 2 + FROM test_dwb_warm_slot_states()') eq 't'; + $proto->psql('postgres', + "SELECT injection_points_wakeup('replay-warm-claimed')"); + usleep(100_000); + } + $proto->poll_query_until('postgres', + 'SELECT claimed = 0 AND sleepers = 2 FROM test_dwb_warm_slot_states()' + ) + or die + 'the pool stayed parked after the head-of-queue scenario; state: ' + . $proto->safe_psql('postgres', + 'SELECT * FROM test_dwb_warm_slot_states()'); +} + SKIP: { skip 'injection points not supported by this build', 3 From 123334a9c41a040afc409a7f0915d81539e28e6b Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 7 Aug 2026 10:48:20 +0300 Subject: [PATCH 47/52] Skip the head-of-queue scenario on Windows It holds a worker still with SIGSTOP to keep it registered on the wait list while the publisher signals, and Windows has no equivalent of that. 019_replslot_limit.pl steps around the same limitation. --- src/test/modules/test_dwb/t/021_replay_warm.pl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/modules/test_dwb/t/021_replay_warm.pl b/src/test/modules/test_dwb/t/021_replay_warm.pl index fd1a0988767c1..d2c67cc4e8311 100644 --- a/src/test/modules/test_dwb/t/021_replay_warm.pl +++ b/src/test/modules/test_dwb/t/021_replay_warm.pl @@ -506,6 +506,11 @@ sub pool_state skip 'injection points not supported by this build', 2 unless $injection_points; + # This scenario holds a process still with SIGSTOP, which Windows has no + # equivalent of. + skip 'stopping and continuing a process is not portable to Windows', 2 + if $PostgreSQL::Test::Utils::windows_os; + # The worse version of the same thing: the worker that dies is the one # the publisher has just woken. A signal is delivered to the head of the # wait list and takes it off that list, so if the head is on its way out, From eb3c1c0f613e509a0776743c4e8f9883767a2b9d Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 7 Aug 2026 11:50:22 +0300 Subject: [PATCH 48/52] Add the ring-size series charts Three assets for the pull request's benchmark comment: what each replay_warm_queue_size buys the standby and costs the pool, and the primary's throughput and latency over each of the four 600 s runs. --- bench/README.md | 6 ++++++ bench/ring-size.png | Bin 0 -> 55618 bytes bench/ring-time-lat.png | Bin 0 -> 85773 bytes bench/ring-time-tps.png | Bin 0 -> 102311 bytes 4 files changed, 6 insertions(+) create mode 100644 bench/ring-size.png create mode 100644 bench/ring-time-lat.png create mode 100644 bench/ring-time-tps.png diff --git a/bench/README.md b/bench/README.md index 65df394e1d0c7..0dffdf88aa178 100644 --- a/bench/README.md +++ b/bench/README.md @@ -24,3 +24,9 @@ and data checksums enabled, converted from the same reference cluster. and the link between them is a real 100 GbE hop (RTT 0.126 ms). The bar chart is throughput per socket count; the two line charts follow the two-socket point over its 600 s run. +* `ring-size.png`, `ring-time-tps.png`, `ring-time-lat.png` — the + `replay_warm_queue_size` series on the two-host pair: what each ring size + buys the standby's replay and what it costs the pool, and the primary's + throughput and latency over each 600 s run at 750 connections against a + synchronous standby. The four points ran back to back on one pair, so + only neighbours are comparable. diff --git a/bench/ring-size.png b/bench/ring-size.png new file mode 100644 index 0000000000000000000000000000000000000000..9613ff630d486bc1fba546953e036a48c3acfc97 GIT binary patch literal 55618 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 literal 0 HcmV?d00001 From f5c0ef5a000b3e6ef508d8ec1143f091afcfeccd Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Fri, 7 Aug 2026 11:56:41 +0300 Subject: [PATCH 49/52] Add the replay time-budget and backlog charts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two assets for the pull request's comment on why the standby falls behind: the backlog it builds over a run at ring 512, and where the single replay process spends its time — waiting against running, and what the running part is made of. --- bench/README.md | 4 ++++ bench/replay-lag-512.png | Bin 0 -> 56005 bytes bench/replay-time-budget.png | Bin 0 -> 67537 bytes 3 files changed, 4 insertions(+) create mode 100644 bench/replay-lag-512.png create mode 100644 bench/replay-time-budget.png diff --git a/bench/README.md b/bench/README.md index 0dffdf88aa178..e1a5ac38d106d 100644 --- a/bench/README.md +++ b/bench/README.md @@ -30,3 +30,7 @@ and data checksums enabled, converted from the same reference cluster. throughput and latency over each 600 s run at 750 connections against a synchronous standby. The four points ran back to back on one pair, so only neighbours are comparable. +* `replay-lag-512.png`, `replay-time-budget.png` — the standby side of the + ring-512 point: the backlog it accumulates over the run, and where the + replay process's time goes — its wall clock split between running and + waiting, and the running part broken down by symbol group. diff --git a/bench/replay-lag-512.png b/bench/replay-lag-512.png new file mode 100644 index 0000000000000000000000000000000000000000..a48098199defa40c9a79f16e89fc58ae7d53cd5c GIT binary patch literal 56005 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 literal 0 HcmV?d00001 diff --git a/bench/replay-time-budget.png b/bench/replay-time-budget.png new file mode 100644 index 0000000000000000000000000000000000000000..14d317367b3347bd60bccbe56a2e5537a48b16b9 GIT binary patch literal 67537 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ҏv-vZ)Z*k`Q52Shi`e$)&5>ZP=gSV8>=ktsW3SL`)p+3r0+yQy8z=k{%io^5xU*&!SWo6b6Prf{(>KNCavvlkmDP6OukRra$PvY@~Z` zrh=SY+J^qGM;8CpM=LF6^Cpu8f3(6n9&%YMU<_7-gy@NLotJS;$lLoq*(?YWe}uZ8 z(3|8+RtrKUyFkjDG)-|TSNvOlc{lM7b;V}!kJW}>fO938 r{;#F^ho?qXNAag?_VpD1$ZOZ-K|9Ux35gNn-{#L*q!&AD^}hcP-ww|e literal 0 HcmV?d00001 From f7fa6574c7609ee079c916cd69a35bf5b9583671 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 8 Aug 2026 11:14:49 +0300 Subject: [PATCH 50/52] Register the double write buffer's exit backstop once per process CREATE DATABASE fails outright when io_torn_pages_protection is double_writes: ERROR: before_shmem_exit callback (0x..., 0x...) is not the latest entry PG_ENSURE_ERROR_CLEANUP registers a callback and cancels it again when the command ends, and the cancellation only works while that callback is still the last one registered. The backstop that hands back a process's staged writes was registered at the first slot the process took, and for a fresh connection that first slot is taken while the template database is copied through the buffer cache -- landing on top of createdb's own callback. Register it in BaseInit, among the other process-wide registrations, which also takes a branch out of the slot path. BaseInit runs in every process that can stage a write: backends, auxiliary processes and background workers alike. --- src/backend/storage/dwb/dwb.c | 36 ++++++++++++++++---------- src/backend/utils/init/postinit.c | 7 +++++ src/include/storage/dwb.h | 1 + src/test/modules/test_dwb/t/001_dwb.pl | 12 +++++++++ 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/backend/storage/dwb/dwb.c b/src/backend/storage/dwb/dwb.c index 4b77384c7debc..ff6ca409ff841 100644 --- a/src/backend/storage/dwb/dwb.c +++ b/src/backend/storage/dwb/dwb.c @@ -61,7 +61,6 @@ typedef struct DWBPendingRef } DWBPendingRef; static DWBPendingRef pendingRefs[2 * DWB_BATCH_MAX_PAGES]; -static bool cleanup_registered = false; /* leader-side meta assembly area, allocated before the seal is attempted */ static DWSlotMeta *leader_metas = NULL; @@ -790,18 +789,6 @@ DWBAcquireSlot(const BufferTag *tag, int wclass, bool use_resowner, if (use_resowner) ResourceOwnerEnlarge(CurrentResourceOwner); - if (!cleanup_registered) - { - /* - * before_shmem_exit, NOT on_proc_exit: dropping the last ref of a - * durable batch publishes its seg_set under LWLocks, which is only - * legal while our PGPROC is alive — on_proc_exit callbacks run - * after ProcKill has released it. - */ - before_shmem_exit(DWBProcExit, 0); - cleanup_registered = true; - } - for (;;) { uint32 idx = pg_atomic_read_u32(&DWBCtl->open_batch_idx[wclass]); @@ -1404,3 +1391,26 @@ DWBProcExit(int code, Datum arg) DWBAbandonRef(&pendingRefs[i]); } } + +/* + * Per-process initialization: arrange for the refs this process is holding + * to be given back if it exits still holding them. + * + * before_shmem_exit, NOT on_proc_exit: dropping the last ref of a durable + * batch publishes its seg_set under LWLocks, which is only legal while our + * PGPROC is alive — on_proc_exit callbacks run after ProcKill has released + * it. + * + * The callback belongs here, among the other process-wide registrations, + * rather than at the first slot a process takes. A command that registers + * a cleanup callback of its own and then cancels it — PG_ENSURE_ERROR_CLEANUP, + * as CREATE DATABASE uses — requires its callback to still be the last one + * registered, and a first write staged through the buffer between the two + * would leave ours on top of it. + */ +void +DWBInitBackend(void) +{ + if (DWBIsEnabled()) + before_shmem_exit(DWBProcExit, 0); +} diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index c86ceefda940b..5375f30ef360c 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -45,6 +45,7 @@ #include "replication/walsender.h" #include "storage/aio_subsys.h" #include "storage/bufmgr.h" +#include "storage/dwb.h" #include "storage/fd.h" #include "storage/ipc.h" #include "storage/lmgr.h" @@ -663,6 +664,12 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * Initialize the double write buffer's process-wide state, so that a + * process holding staged writes when it exits gives them back. + */ + DWBInitBackend(); } diff --git a/src/include/storage/dwb.h b/src/include/storage/dwb.h index 91baa17ed0239..e8fc83b5892c1 100644 --- a/src/include/storage/dwb.h +++ b/src/include/storage/dwb.h @@ -510,6 +510,7 @@ extern Size DWBShmemSize(void); extern void DWBShmemInit(void); /* dwb.c — write path */ +extern void DWBInitBackend(void); extern void DWBStagePageWrite(const BufferTag *tag, const char *image, XLogRecPtr page_lsn, DWBSlotRef *ref); extern void DWBStagePageWriteNoWait(const BufferTag *tag, const char *image, diff --git a/src/test/modules/test_dwb/t/001_dwb.pl b/src/test/modules/test_dwb/t/001_dwb.pl index 0285752e60935..1d4b7b21524f5 100644 --- a/src/test/modules/test_dwb/t/001_dwb.pl +++ b/src/test/modules/test_dwb/t/001_dwb.pl @@ -155,6 +155,18 @@ sub flip_byte is($node->safe_psql('postgres', 'SELECT test_dwb_retire()'), '1', 'batch published from the exit backstop retires'); +# A command that registers an exit callback of its own and cancels it again +# on the way out -- PG_ENSURE_ERROR_CLEANUP, as CREATE DATABASE uses -- can +# only cancel it while it is still the last one registered. The backstop +# above is registered once, when the process starts, so that a write staged +# in between does not land on top of it. This session has staged none yet, +# which is exactly the case that would. +$node->safe_psql('postgres', 'CREATE DATABASE dwb_createdb'); +is( $node->safe_psql('dwb_createdb', 'SELECT count(*) FROM pg_class'), + $node->safe_psql('template1', 'SELECT count(*) FROM pg_class'), + 'a database copied through the buffer cache is complete and usable'); +$node->safe_psql('postgres', 'DROP DATABASE dwb_createdb'); + # --- transaction abort releases refs (ResourceOwner path) --------------- # An ERROR with unpublished refs: the abort poisons the slots, and the From 8b972605b926c889d74c5b83ceeaee9cdb5af3f5 Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sat, 8 Aug 2026 11:22:32 +0300 Subject: [PATCH 51/52] Declare the warm pool's hooks where smgr.c calls them smgrdounlinkall and smgrtruncate call XLogWarmDropBegin and XLogWarmDropEnd without a declaration in scope, which the compiler accepts with a warning and a guessed prototype. Include the header that declares them. --- src/backend/storage/smgr/smgr.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c index 591faf22892db..017eb2cf8f858 100644 --- a/src/backend/storage/smgr/smgr.c +++ b/src/backend/storage/smgr/smgr.c @@ -64,6 +64,7 @@ #include "postgres.h" #include "access/xlogutils.h" +#include "access/xlogwarm.h" #include "lib/ilist.h" #include "miscadmin.h" #include "storage/aio.h" From 9daebc687a277663e0505e122cc3ae8eed71046e Mon Sep 17 00:00:00 2001 From: Vadim Ponomarev Date: Sun, 9 Aug 2026 21:18:21 +0300 Subject: [PATCH 52/52] Align the one page buffer this test hands to smgr test_dwb_checkpoint_pending reads and writes a block through smgr, which may reach the file with direct I/O and requires a buffer aligned for it. The buffer was declared with block alignment only, and an assert build dies on the first call -- the standby is killed by the assertion in buffers_to_iovec and the test hangs waiting for it, which takes the whole tree's check-world down with it. Give that one buffer PGIOAlignedBlock. The other page buffers here are read and written through this test's own descriptors, where block alignment is all that is asked for, and the comment says which is which. Nothing outside the test needs changing: every buffer the double write buffer itself hands to smgr is allocated with palloc_aligned, its staging area is aligned where it is carved out of shared memory, and its one raw write opens the file without direct I/O. --- src/test/modules/test_dwb/test_dwb.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/test/modules/test_dwb/test_dwb.c b/src/test/modules/test_dwb/test_dwb.c index 99eccba2ecfe8..d4569681ebcd8 100644 --- a/src/test/modules/test_dwb/test_dwb.c +++ b/src/test/modules/test_dwb/test_dwb.c @@ -694,7 +694,14 @@ test_dwb_checkpoint_pending(PG_FUNCTION_ARGS) BufferTag tag = make_tag(MyDatabaseId, relnumber, 0); DWBSlotRef ref; SMgrRelation reln; - static PGAlignedBlock image; + + /* + * This one goes through smgr, which may reach the file with direct I/O + * and requires the alignment that needs. The other page buffers here are + * read and written through this test's own descriptors, where the block + * alignment is all that is asked for. + */ + static PGIOAlignedBlock image; check_dwb_enabled();

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�^W_m?+G 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 literal 58088 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 K#WaOe*Z&`|+J0>S diff --git a/bench/time-tps.png b/bench/time-tps.png index f8c033ddaebceaa03ebad6771a4cc64f06313cce..4e659a0d1a7ef443dd7860f7202bd181b90335d6 100644 GIT binary patch literal 55703 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